Files
overleaf-cep/services/history-v1/api/schema.js
T
Jakob Ackermann 49969f6e0d [history-v1] adopt strict zod types and extend test coverage (#36089)
GitOrigin-RevId: 7bdffe2c05c7c17be6eb3e4296d8df05d650c53e
2026-09-17 11:49:08 +00:00

525 lines
14 KiB
JavaScript

'use strict'
const { z, zz } = require('@overleaf/validation-tools')
const Blob = require('overleaf-editor-core').Blob
const {
rawSnapshot,
rawChange,
rawBlobHash,
rawFileMetadata,
} = require('overleaf-editor-core/lib/schemas')
const hexHashPattern = new RegExp(Blob.HEX_HASH_RX_STRING)
// Rollout-temporary fallback building blocks (main's pre-refinement shapes
// for imported snapshots/changes, before they were validated against
// overleaf-editor-core's raw* schemas); delete alongside the
// importSnapshotFallbackSchema/importChangesFallbackSchema opts below when
// this route's REQ_VALIDATION_MODE instrumentation is removed.
const fallbackFileSchema = z
.object({
hash: z.string().optional(),
rangesHash: z.string().optional(),
byteLength: z.number().int().nullable().optional(),
stringLength: z.number().int().nullable().optional(),
metadata: z.object({}).passthrough().optional(),
})
.passthrough()
const fallbackV2DocVersionsSchema = z.object({
pathname: z.string().optional(),
v: z.number().int().optional(),
})
const fallbackSnapshotSchema = z.object({
files: z.record(z.string(), fallbackFileSchema),
projectVersion: z.string().optional(),
v2DocVersions: z
.record(z.string(), fallbackV2DocVersionsSchema)
.nullable()
.optional(),
timestamp: z.string().optional(),
})
const fallbackOperationSchema = z
.object({
pathname: z.string().optional(),
newPathname: z.string().optional(),
blob: z
.object({
hash: z.string(),
})
.optional(),
textOperation: z.array(z.any()).optional(),
file: fallbackFileSchema.optional(),
contentHash: z.string().optional(),
})
.passthrough()
const fallbackOriginSchema = z
.object({
kind: z.string().optional(),
})
.passthrough()
const fallbackChangeSchema = z
.object({
timestamp: z.string(),
operations: z.array(fallbackOperationSchema),
authors: z.array(z.number().int().nullable()).optional(),
v2Authors: z.array(z.string().nullable()).optional(),
origin: fallbackOriginSchema.optional(),
projectVersion: z.string().optional(),
v2DocVersions: z.record(z.string(), fallbackV2DocVersionsSchema).optional(),
})
.passthrough()
const setContentBodyBase = z.object({
pathname: z.string().min(1),
source: z.string(),
userId: zz.objectId().optional(),
timestamp: zz.datetime(),
metadata: rawFileMetadata.optional(),
trackChanges: z.boolean().optional(),
})
const schemas = {
projectId: z.object({
params: z
.object({
project_id: zz.projectHistoryId().optional(),
})
.optional(),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
projectIdFallbackSchema: z.object({
params: z
.object({
project_id: z.string().optional(),
})
.optional(),
}),
initializeProject: z.object({
body: z
.strictObject({
projectId: zz.projectHistoryId().optional(),
})
.optional(),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
initializeProjectFallbackSchema: z.object({
body: z
.object({
projectId: z.string().optional(),
})
.optional(),
}),
cloneProject: z.object({
body: z.strictObject({
targetProjectId: zz.projectHistoryId(),
}),
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
cloneProjectFallbackSchema: z.object({
body: z.object({
targetProjectId: z.string(),
}),
params: z.object({
project_id: z.string(),
}),
}),
getProjectBlobsStats: z.object({
body: z.strictObject({
projectIds: z.array(zz.projectHistoryId()),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getProjectBlobsStatsFallbackSchema: z.object({
body: z.object({
projectIds: z.array(z.string()),
}),
}),
getBlobStats: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
body: z.strictObject({
blobHashes: z.array(rawBlobHash),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getBlobStatsFallbackSchema: 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: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
deleteProjectFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
getProjectBlob: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
hash: z.string().regex(hexHashPattern),
}),
headers: z.object({
range: z.string().optional(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getProjectBlobFallbackSchema: 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: zz.projectHistoryId(),
hash: z.string().regex(hexHashPattern),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
headProjectBlobFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
hash: z.string().regex(hexHashPattern),
}),
}),
createProjectBlob: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
hash: z.string().regex(hexHashPattern),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
createProjectBlobFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
hash: z.string().regex(hexHashPattern),
}),
}),
copyProjectBlob: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
hash: z.string().regex(hexHashPattern),
}),
query: z.object({
copyFrom: zz.projectHistoryId(),
sizeLimit: z.coerce.number().optional(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
copyProjectBlobFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
hash: z.string().regex(hexHashPattern),
}),
query: z.object({
copyFrom: z.string(),
sizeLimit: z.coerce.number().optional(),
}),
}),
getLatestContent: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getLatestContentFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
getLatestHashedContent: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getLatestHashedContentFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
getLatestHistory: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getLatestHistoryFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
getLatestHistoryRaw: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
query: z.object({
readOnly: z.coerce.boolean().optional(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getLatestHistoryRawFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
query: z.object({
readOnly: z.coerce.boolean().optional(),
}),
}),
getLatestPersistedHistory: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
getHistory: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
version: z.coerce.number(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getHistoryFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
version: z.coerce.number(),
}),
}),
getContentAtVersion: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
version: z.coerce.number(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getContentAtVersionFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
version: z.coerce.number(),
}),
}),
getHistoryBefore: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
timestamp: zz.datetime(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getHistoryBeforeFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
timestamp: zz.datetime(),
}),
}),
getLatestZip: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getLatestZipFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
getZip: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
version: z.coerce.number(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getZipFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
version: z.coerce.number(),
}),
}),
createZip: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
version: z.coerce.number(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
createZipFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
version: z.coerce.number(),
}),
}),
getChanges: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
query: z.object({
since: z.coerce.number().optional(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
getChangesFallbackSchema: 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: zz.projectHistoryId(),
}),
body: rawSnapshot,
}),
// Rollout-temporary fallback (pre-refinement schema from main, before
// imported snapshots were validated against overleaf-editor-core's
// rawSnapshot); delete when this route's REQ_VALIDATION_MODE
// instrumentation is removed.
importSnapshotFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
body: fallbackSnapshotSchema,
}),
importChanges: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
query: z.object({
end_version: z.coerce.number(),
return_snapshot: z.enum(['hashed', 'none']).optional(),
}),
body: z.array(rawChange),
}),
// Rollout-temporary fallback (pre-refinement schema from main, before
// imported changes were validated against overleaf-editor-core's
// rawChange); delete when this route's REQ_VALIDATION_MODE
// instrumentation is removed.
importChangesFallbackSchema: 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(fallbackChangeSchema),
}),
setContent: z.object({
params: z.object({
project_id: z.string(),
}),
// strict() rejects unknown keys, so a mixed payload with both content and
// blobHash fails both union members and is rejected.
body: z
.union([
setContentBodyBase.extend({ content: z.string() }).strict(),
setContentBodyBase
.extend({ blobHash: z.string().regex(hexHashPattern) })
.strict(),
])
.refine(
body => {
if (body.trackChanges && !body.userId) return false
return true
},
{
message: '"userId" is required when trackChanges is set',
path: ['userId'],
}
),
}),
flushChanges: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
flushChangesFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
expireProject: z.object({
params: z.object({
project_id: zz.projectHistoryId(),
}),
}),
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
expireProjectFallbackSchema: z.object({
params: z.object({
project_id: z.string(),
}),
}),
}
module.exports = schemas