From c25e49782fab1effb0e68a10c491dea99e68454f Mon Sep 17 00:00:00 2001 From: Anna Claire Fields <58918237+franklovefrank@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:33:02 +0100 Subject: [PATCH] Merge pull request #29965 from overleaf/acf-migration1-validation Add Zod validation (replaces swagger-validator + swagger-metadata) GitOrigin-RevId: 2e4ed742e401bdfe49c6f7dc9d0fceeba20cfc7f --- package-lock.json | 1 + services/history-v1/Dockerfile | 2 + services/history-v1/Makefile | 1 + services/history-v1/api/schema.js | 222 ++++++++++++++++++++++++++++++ services/history-v1/package.json | 1 + 5 files changed, 227 insertions(+) create mode 100644 services/history-v1/api/schema.js diff --git a/package-lock.json b/package-lock.json index dbef411cbb..021ced053a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54683,6 +54683,7 @@ "@overleaf/redis-wrapper": "*", "@overleaf/settings": "*", "@overleaf/stream-utils": "^0.1.0", + "@overleaf/validation-tools": "*", "archiver": "^5.3.0", "basic-auth": "^2.0.1", "bluebird": "^3.7.2", diff --git a/services/history-v1/Dockerfile b/services/history-v1/Dockerfile index 18105972c4..6695b98dfe 100644 --- a/services/history-v1/Dockerfile +++ b/services/history-v1/Dockerfile @@ -29,6 +29,7 @@ COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/pack COPY libraries/redis-wrapper/package.json /overleaf/libraries/redis-wrapper/package.json COPY libraries/settings/package.json /overleaf/libraries/settings/package.json COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json +COPY libraries/validation-tools/package.json /overleaf/libraries/validation-tools/package.json COPY services/history-v1/package.json /overleaf/services/history-v1/package.json COPY tools/migrations/package.json /overleaf/tools/migrations/package.json COPY patches/ /overleaf/patches/ @@ -46,6 +47,7 @@ COPY libraries/promise-utils/ /overleaf/libraries/promise-utils/ COPY libraries/redis-wrapper/ /overleaf/libraries/redis-wrapper/ COPY libraries/settings/ /overleaf/libraries/settings/ COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/ +COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/ COPY services/history-v1/ /overleaf/services/history-v1/ COPY tools/migrations/ /overleaf/tools/migrations/ diff --git a/services/history-v1/Makefile b/services/history-v1/Makefile index d68c1f91de..482e2607df 100644 --- a/services/history-v1/Makefile +++ b/services/history-v1/Makefile @@ -26,6 +26,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \ $(MONOREPO)/libraries/redis-wrapper/package.json \ $(MONOREPO)/libraries/settings/package.json \ $(MONOREPO)/libraries/stream-utils/package.json \ + $(MONOREPO)/libraries/validation-tools/package.json \ $(MONOREPO)/services/history-v1/package.json \ $(MONOREPO)/tools/migrations/package.json \ $(MONOREPO)/patches/* \ diff --git a/services/history-v1/api/schema.js b/services/history-v1/api/schema.js new file mode 100644 index 0000000000..a669300a9b --- /dev/null +++ b/services/history-v1/api/schema.js @@ -0,0 +1,222 @@ +'use strict' + +const { z } = require('@overleaf/validation-tools') +const Blob = require('overleaf-editor-core').Blob + +const hexHashPattern = new RegExp(Blob.HEX_HASH_RX_STRING) + +const fileSchema = z.object({ + hash: z.string().optional(), + byteLength: z.number().int().nullable().optional(), + stringLength: z.number().int().nullable().optional(), +}) + +const snapshotSchema = z.object({ + files: z.record(z.string(), fileSchema), +}) + +const v2DocVersionsSchema = z.object({ + pathname: z.string().optional(), + v: z.number().int().optional(), +}) + +const operationSchema = z.object({ + pathname: z.string().optional(), + newPathname: z.string().optional(), + blob: z + .object({ + hash: z.string(), + }) + .optional(), + textOperation: z.array(z.any()).optional(), + file: fileSchema.optional(), +}) + +const changeSchema = z.object({ + timestamp: z.string(), + operations: z.array(operationSchema), + authors: z.array(z.number().int().nullable()).optional(), + v2Authors: z.array(z.string().nullable()).optional(), + projectVersion: z.string().optional(), + v2DocVersions: z.record(v2DocVersionsSchema).optional(), +}) + +const schemas = { + initializeProject: z.object({ + body: z + .object({ + projectId: z.string().optional(), + }) + .optional(), + }), + + getProjectBlobsStats: z.object({ + body: z.object({ + projectIds: z.array(z.string()), + }), + }), + + getBlobStats: z.object({ + params: z.object({ + project_id: z.string(), + }), + body: z.object({ + blobHashes: z.array(z.string()), + }), + }), + + deleteProject: z.object({ + params: z.object({ + project_id: z.string(), + }), + body: z.any().optional(), + }), + + getProjectBlob: z.object({ + params: z.object({ + project_id: z.string(), + hash: z.string().regex(hexHashPattern), + }), + headers: z.object({ + range: z.string().optional(), + }), + }), + + headProjectBlob: z.object({ + params: z.object({ + project_id: z.string(), + hash: z.string().regex(hexHashPattern), + }), + }), + + createProjectBlob: z.object({ + params: z.object({ + project_id: z.string(), + hash: z.string().regex(hexHashPattern), + }), + body: z.any().optional(), + }), + + copyProjectBlob: z.object({ + params: z.object({ + project_id: z.string(), + hash: z.string().regex(hexHashPattern), + }), + query: z.object({ + copyFrom: z.string(), + }), + body: z.any().optional(), + }), + + getLatestContent: z.object({ + params: z.object({ + project_id: z.string(), + }), + }), + + getLatestHashedContent: z.object({ + params: z.object({ + project_id: z.string(), + }), + }), + + getLatestHistory: z.object({ + params: z.object({ + project_id: z.string(), + }), + }), + + getLatestHistoryRaw: z.object({ + params: z.object({ + project_id: z.string(), + }), + query: z.object({ + readOnly: z.boolean().optional(), + }), + }), + + getLatestPersistedHistory: z.object({ + params: z.object({ + project_id: z.string(), + }), + }), + + getHistory: z.object({ + params: z.object({ + project_id: z.string(), + version: z.coerce.number(), + }), + }), + + getContentAtVersion: z.object({ + params: z.object({ + project_id: z.string(), + version: z.coerce.number(), + }), + }), + + getHistoryBefore: z.object({ + params: z.object({ + project_id: z.string(), + timestamp: z.iso.datetime(), + }), + }), + + getZip: z.object({ + params: z.object({ + project_id: z.string(), + version: z.coerce.number(), + }), + }), + + createZip: z.object({ + params: z.object({ + project_id: z.string(), + version: z.coerce.number(), + }), + body: z.any().optional(), + }), + + getChanges: z.object({ + params: z.object({ + project_id: z.string(), + }), + query: z.object({ + since: z.coerce.number().optional(), + }), + }), + + importSnapshot: z.object({ + params: z.object({ + project_id: z.string(), + }), + body: snapshotSchema, + }), + + importChanges: z.object({ + params: z.object({ + project_id: z.string(), + }), + query: z.object({ + end_version: z.coerce.number(), + return_snapshot: z.enum(['hashed', 'none']).optional(), + }), + body: z.array(changeSchema), + }), + + flushChanges: z.object({ + params: z.object({ + project_id: z.string(), + }), + body: z.any().optional(), + }), + + expireProject: z.object({ + params: z.object({ + project_id: z.string(), + }), + body: z.any().optional(), + }), +} + +module.exports = schemas diff --git a/services/history-v1/package.json b/services/history-v1/package.json index 7cedcbef51..b56e2db1a1 100644 --- a/services/history-v1/package.json +++ b/services/history-v1/package.json @@ -17,6 +17,7 @@ "@overleaf/redis-wrapper": "*", "@overleaf/settings": "*", "@overleaf/stream-utils": "^0.1.0", + "@overleaf/validation-tools": "*", "archiver": "^5.3.0", "basic-auth": "^2.0.1", "bluebird": "^3.7.2",