Browse Source

Add build trigger for validation-tools

GitOrigin-RevId: 04299d9ab23c65aa791acecd1c0e63b70df9a8d1
Andrew Rumble 11 months ago
parent
commit
7962206e22

+ 21 - 0
.editorconfig

@@ -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

+ 1 - 0
libraries/validation-tools/.nvmrc

@@ -0,0 +1 @@
+22.18.0

+ 5 - 0
libraries/validation-tools/Errors.js

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

+ 10 - 0
libraries/validation-tools/buildscript.txt

@@ -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

+ 11 - 0
libraries/validation-tools/index.js

@@ -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,
+}

+ 31 - 0
libraries/validation-tools/package.json

@@ -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"
+  }
+}

+ 85 - 0
libraries/validation-tools/test/unit/src/validateReq.test.ts

@@ -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'] }),
+          ]),
+        })
+      )
+    })
+  })
+})

+ 44 - 0
libraries/validation-tools/test/unit/src/zodHelpers.test.ts

@@ -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')
+    })
+  })
+})

+ 11 - 0
libraries/validation-tools/tsconfig.json

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

+ 34 - 0
libraries/validation-tools/validateReq.js

@@ -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 }

+ 17 - 0
libraries/validation-tools/zodHelpers.js

@@ -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 }