Add build trigger for validation-tools

GitOrigin-RevId: 04299d9ab23c65aa791acecd1c0e63b70df9a8d1
This commit is contained in:
Andrew Rumble
2025-09-15 17:40:47 +01:00
committed by Copybot
parent b30740e71c
commit 7962206e22
11 changed files with 270 additions and 0 deletions

21
.editorconfig Normal file
View File

@@ -0,0 +1,21 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[Makefile]
indent_style = tab
[*.go]
indent_style = tab
[*.{pug,coffee}]
indent_style = tab
[*.{pug,patch}]
trim_trailing_whitespace = false

View File

@@ -0,0 +1 @@
22.18.0

View File

@@ -0,0 +1,5 @@
const OError = require('@overleaf/o-error')
class ParamsError extends OError {}
module.exports = { ParamsError }

View File

@@ -0,0 +1,10 @@
validation-tools
--dependencies=None
--docker-repos=us-east1-docker.pkg.dev/overleaf-ops/ol-docker
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--public-repo=False
--script-version=4.7.0

View File

@@ -0,0 +1,11 @@
const { ParamsError } = require('./Errors')
const { z } = require('zod')
const { zz } = require('./zodHelpers')
const { validateReq } = require('./validateReq')
module.exports = {
z,
zz,
validateReq,
ParamsError,
}

View File

@@ -0,0 +1,31 @@
{
"name": "@overleaf/validation-tools",
"homepage": "www.overleaf.com",
"description": "Validation tools that can be used in a service.",
"repository": {
"type": "git",
"url": "https://github.com/overleaf/overleaf"
},
"main": "index.js",
"license": "AGPL-3.0-only",
"version": "1.0.0",
"scripts": {
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"test:ci": "npm run test:unit",
"test:unit": "vitest test/unit/src/*.test.ts --isolate=false",
"types:check": "tsc --noEmit"
},
"dependencies": {
"@overleaf/o-error": "*",
"mongodb": "^6.12.0",
"zod": "^4.1.8"
},
"devDependencies": {
"typescript": "^5.0.4",
"vitest": "^3.2.4"
}
}

View File

@@ -0,0 +1,85 @@
import { validateReq } from '../../../validateReq'
import { describe, expect, it } from 'vitest'
import { z } from 'zod'
import type { Request } from 'express'
import { zz } from '../../../zodHelpers'
describe('validateReq', () => {
describe('with a request that is valid for the schema', () => {
it('should return the parsed request', () => {
const req = {
params: {
id: '507f1f77bcf86cd799439011',
},
body: {
name: 'Valid Name',
},
} as Request<{ id: string }, any, { name: string }>
const schema = z.object({
params: z.object({
id: zz.objectId(),
}),
body: z.object({
name: z.string(),
}),
})
const result = validateReq(req, schema)
expect(result).toEqual({
params: {
id: '507f1f77bcf86cd799439011',
},
body: {
name: 'Valid Name',
},
})
})
})
describe('with a request that is not valid for the schema', () => {
it('should throw NotFoundError if params are invalid', () => {
const req = {
params: {
id: 'invalid-object-id',
},
} as Request<{ id: string }>
expect(() =>
validateReq(
req,
z.object({
params: z.object({
id: zz.objectId(),
}),
})
)
).toThrowError(expect.objectContaining({ name: 'ParamsError' }))
})
it('should throw an error containing issues if the schema is invalid', () => {
const req = {
body: {
name: 1234,
},
} as Request
expect(() =>
validateReq(
req,
z.object({
body: z.object({
name: z.string(),
}),
})
)
).toThrowError(
expect.objectContaining({
issues: expect.arrayContaining([
expect.objectContaining({ path: ['body', 'name'] }),
]),
})
)
})
})
})

View File

@@ -0,0 +1,44 @@
import { zz } from '../../../zodHelpers'
import { describe, expect, it } from 'vitest'
import mongodb from 'mongodb'
const { ObjectId } = mongodb
describe('zodHelpers', () => {
describe('objectId', () => {
it('fails to parse when provided with an invalid ObjectId', () => {
const parsed = zz.objectId().safeParse('aa')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'invalid Mongo ObjectId',
}),
])
})
it('parses successfully when provided with a valid ObjectId', () => {
const parsed = zz.objectId().safeParse('507f1f77bcf86cd799439011')
expect(parsed.success).toBe(true)
expect(parsed.data).toBe('507f1f77bcf86cd799439011')
})
})
describe('coercedObjectId', () => {
it('fails to parse when provided with an invalid ObjectId', () => {
const parsed = zz.coercedObjectId().safeParse('aa')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'invalid Mongo ObjectId',
}),
])
})
it('parses to an ObjectId when provided with a valid ObjectId string', () => {
const parsed = zz.coercedObjectId().safeParse('507f1f77bcf86cd799439011')
expect(parsed.success).toBe(true)
expect(parsed.data).toBeInstanceOf(ObjectId)
expect(parsed.data?.toString()).toBe('507f1f77bcf86cd799439011')
})
})
})

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"allowImportingTsExtensions": true
},
"include": [
"**/*.js",
"**/*.ts",
"**/*.cjs"
]
}

View File

@@ -0,0 +1,34 @@
// @ts-check
const { ParamsError } = require('./Errors')
/**
* @typedef {import('zod').ZodType} ZodType
* @typedef {import('express').Request} Request
*/
/**
* @template T
* @typedef {import('zod').output<T>} output<T>
*/
/**
* Validate a request against a zod schema
*
* @template {ZodType} T
* @param {Request} req - The Express request object
* @param {T} schema - The Zod schema to validate against
* @returns {output<T>} The validated request object
*/
function validateReq(req, schema) {
const parsed = schema.safeParse(req)
if (parsed.success) {
return parsed.data
} else if (parsed.error.issues.some(issue => issue.path[0] === 'params')) {
// Parts of the URL path failed to validate; throw a specific error
throw new ParamsError('Invalid params').withCause(parsed.error)
} else {
throw parsed.error
}
}
module.exports = { validateReq }

View File

@@ -0,0 +1,17 @@
const { z } = require('zod')
const mongodb = require('mongodb')
const { ObjectId } = mongodb
const zz = {
objectId: () =>
z.string().refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' }),
coercedObjectId: () =>
z
.string()
.refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' })
.transform(val => new ObjectId(val)),
hex: () => z.string().regex(/^[0-9a-f]*$/),
}
module.exports = { zz }