From 81ad010b4112a688de69584dac4eba039a746883 Mon Sep 17 00:00:00 2001 From: Zeyu Zhang <39144422+zeyu2001@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:58:08 +0800 Subject: [PATCH] feat: createUrlSchema --- .github/workflows/release.yml | 3 +- apps/docs/package.json | 4 +- etc/starter-kitty-validators.api.md | 4 ++ package.json | 1 + packages/safe-fs/package.json | 1 + packages/validators/package.json | 3 +- packages/validators/src/__tests__/url.test.ts | 40 ++++++++++++++++++- packages/validators/src/url/index.ts | 24 +++++++++-- packages/validators/src/url/schema.ts | 2 +- pnpm-lock.yaml | 6 +++ turbo.json | 5 ++- 11 files changed, 83 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45c3651..9299e42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - "v*" + - "*/v*" workflow_dispatch: {} permissions: @@ -56,4 +56,3 @@ jobs: run: pnpm -r publish --no-git-checks --tag latest --access public env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - \ No newline at end of file diff --git a/apps/docs/package.json b/apps/docs/package.json index 19c8243..768b5d8 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -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" } diff --git a/etc/starter-kitty-validators.api.md b/etc/starter-kitty-validators.api.md index 57f9e70..6878be4 100644 --- a/etc/starter-kitty-validators.api.md +++ b/etc/starter-kitty-validators.api.md @@ -8,6 +8,7 @@ import { z } from 'zod'; import { ZodSchema } from 'zod'; +import { ZodTypeDef } from 'zod'; // @public export const createEmailSchema: (options?: EmailValidatorOptions) => ZodSchema; @@ -15,6 +16,9 @@ export const createEmailSchema: (options?: EmailValidatorOptions) => ZodSchema ZodSchema; +// @public +export const createUrlSchema: (options?: UrlValidatorOptions) => ZodSchema; + // @public export interface EmailValidatorOptions { domains?: { diff --git a/package.json b/package.json index 4accf5f..a4607fa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/safe-fs/package.json b/packages/safe-fs/package.json index 94b077a..62a820d 100644 --- a/packages/safe-fs/package.json +++ b/packages/safe-fs/package.json @@ -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", diff --git a/packages/validators/package.json b/packages/validators/package.json index d576700..c79527d 100644 --- a/packages/validators/package.json +++ b/packages/validators/package.json @@ -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": [ @@ -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", diff --git a/packages/validators/src/__tests__/url.test.ts b/packages/validators/src/__tests__/url.test.ts index a6edd23..3c739c4 100644 --- a/packages/validators/src/__tests__/url.test.ts +++ b/packages/validators/src/__tests__/url.test.ts @@ -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', () => { @@ -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:test@test.test')).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) + }) +}) diff --git a/packages/validators/src/url/index.ts b/packages/validators/src/url/index.ts index 1c912b3..2d34de5 100644 --- a/packages/validators/src/url/index.ts +++ b/packages/validators/src/url/index.ts @@ -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, @@ -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()) @@ -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 => { + const result = optionsSchema.safeParse({ ...defaultOptions, ...options }) + if (result.success) { + return toSchema(result.data) + } + throw new OptionsError(fromError(result.error).toString()) +} diff --git a/packages/validators/src/url/schema.ts b/packages/validators/src/url/schema.ts index 06976c2..5804d18 100644 --- a/packages/validators/src/url/schema.ts +++ b/packages/validators/src/url/schema.ts @@ -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)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf2f9da..41c9c1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: apps/docs: devDependencies: + '@opengovsg/starter-kitty-fs': + specifier: workspace:* + version: link:../../packages/safe-fs + '@opengovsg/starter-kitty-validators': + specifier: workspace:* + version: link:../../packages/validators vitepress: specifier: ^1.3.1 version: 1.3.1(@algolia/client-search@4.24.0)(@types/node@18.19.47)(postcss@8.4.39)(search-insights@2.15.0)(typescript@5.4.5) diff --git a/turbo.json b/turbo.json index df23f7e..7f3b50b 100644 --- a/turbo.json +++ b/turbo.json @@ -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"] },