// @ts-check 'use strict' const { z, zz } = require('@overleaf/validation-tools') const rawTrackingProps = z.strictObject({ type: z.enum(['insert', 'delete']), userId: zz.objectId(), ts: z.iso.datetime(), }) const rawClearTrackingProps = z.strictObject({ type: z.literal('none'), }) const rawInsertOp = z.union([ z.strictObject({ i: z.string(), commentIds: z.array(zz.objectId()).optional(), tracking: rawTrackingProps.optional(), }), z.string(), ]) const rawRemoveOp = z.number().int().max(-1) const rawRetainOp = z.union([ z.strictObject({ r: z.number().int().min(1), commentIds: z.array(zz.objectId()).optional(), tracking: z .discriminatedUnion('type', [rawTrackingProps, rawClearTrackingProps]) .optional(), }), z.number().int().min(1), ]) const rawScanOp = z.union([rawInsertOp, rawRemoveOp, rawRetainOp]) const rawTextOperation = z.strictObject({ textOperation: z.array(rawScanOp), contentHash: z.string().optional(), }) const rawRange = z.strictObject({ pos: z.number().int().min(0), length: z.number().int().min(0), }) const rawAddCommentOperation = z.strictObject({ commentId: zz.objectId(), ranges: z.array(rawRange), resolved: z.boolean().optional(), }) const rawSetCommentStateOperation = z.strictObject({ commentId: zz.objectId(), resolved: z.boolean(), }) const rawDeleteCommentOperation = z.strictObject({ deleteComment: zz.objectId(), }) const rawEditNoOperation = z.strictObject({ noOp: z.literal(true), }) const rawEditOperation = z.union([ rawTextOperation, rawAddCommentOperation, rawDeleteCommentOperation, rawSetCommentStateOperation, rawEditNoOperation, ]) const rawComment = z.strictObject({ id: zz.objectId(), ranges: z.array(rawRange), resolved: z.boolean().optional(), }) const rawTrackedChange = z.strictObject({ range: rawRange, tracking: rawTrackingProps, }) const rawStringFileData = z.strictObject({ content: z.string(), comments: z.array(rawComment).optional(), trackedChanges: z.array(rawTrackedChange).optional(), }) // linkedFileData as written by the web linked-file agents (all-scalar; see // the per-provider _sanitizeData implementations in web). importedAt is // always written on create/refresh but may be absent on very old documents. const rawLinkedFileData = z.discriminatedUnion('provider', [ z.strictObject({ provider: z.literal('url'), url: z.string(), importedAt: z.iso.datetime().optional(), }), z.strictObject({ provider: z.literal('project_file'), source_project_id: zz.objectId().optional(), v1_source_doc_id: z.number().optional(), source_entity_path: z.string(), importedAt: z.iso.datetime().optional(), }), z.strictObject({ provider: z.literal('project_output_file'), source_project_id: zz.objectId().optional(), v1_source_doc_id: z.number().optional(), source_output_file_path: z.string(), build_id: zz.buildId().optional(), clsiServerId: z.string().optional(), importedAt: z.iso.datetime().optional(), }), z.strictObject({ provider: z.literal('mendeley'), // null, not just absent, for an import from a personal library: the web // agents always build the data object with a group_id key, so no group // means `group_id: undefined`, and mongoose keeps that key when $set-ing // the Mixed linkedFileData (upserting over an existing file), where the // driver serializes it as null. Same for the zotero/papers branches. group_id: zz.routeSegment().nullish(), importer_id: z.string().optional(), v1_importer_id: z.number().optional(), importedAt: z.iso.datetime().optional(), }), z.strictObject({ provider: z.literal('zotero'), // absent on files imported before the `format` field existed; agents // default this to 'bibtex' (ZoteroAgent `_getFormat`) format: z.enum(['bibtex', 'biblatex']).optional(), group_id: zz.routeSegment().nullish(), importer_id: z.string().optional(), v1_importer_id: z.number().optional(), importedAt: z.iso.datetime().optional(), }), z.strictObject({ provider: z.literal('papers'), group_id: zz.routeSegment().nullish(), importer_id: z.string().optional(), v1_importer_id: z.number().optional(), importedAt: z.iso.datetime().optional(), }), ]) const rawBlobHash = z.string().regex(/^[0-9a-f]{40}$/, { message: 'invalid blob hash', }) const rawFileMetadata = z.union([ // doc/clear metadata z.strictObject({}), // regular file v2 z.strictObject({ importedAt: z.iso.datetime(), }), // linked-file v2 rawLinkedFileData, // main-file v1 z.strictObject({ main: z.boolean(), importedAt: z.iso.datetime().optional(), }), // url import v1 z.strictObject({ agent: z.literal('url'), agentDataId: z.number(), importedAt: z.iso.datetime().optional(), }), ]) const rawHashFileData = z.strictObject({ hash: rawBlobHash, rangesHash: rawBlobHash.optional(), }) const rawBinaryFileData = z.strictObject({ hash: rawBlobHash, byteLength: z.number().int().min(0), }) const rawLazyStringFileData = z.strictObject({ hash: rawBlobHash, stringLength: z.number().int().min(0), rangesHash: rawBlobHash.optional(), operations: z.array(rawEditOperation).optional(), }) const rawHollowBinaryFileData = z.strictObject({ byteLength: z.number().int().min(0), }) const rawHollowStringFileData = z.strictObject({ stringLength: z.number().int().min(0), }) const rawFileData = z.union([ rawBinaryFileData, rawHashFileData, rawHollowBinaryFileData, rawHollowStringFileData, rawLazyStringFileData, rawStringFileData, ]) // the strict variants disambiguate the union by their key sets const rawFile = z.union([ rawBinaryFileData.extend({ metadata: rawFileMetadata.optional() }), rawHashFileData.extend({ metadata: rawFileMetadata.optional() }), rawHollowBinaryFileData.extend({ metadata: rawFileMetadata.optional() }), rawHollowStringFileData.extend({ metadata: rawFileMetadata.optional() }), rawLazyStringFileData.extend({ metadata: rawFileMetadata.optional() }), rawStringFileData.extend({ metadata: rawFileMetadata.optional() }), ]) const rawFileMap = z.record(z.string(), rawFile) const rawV2DocVersions = z.record( z.string(), z.strictObject({ pathname: z.string(), v: z.number().int(), }) ) const rawSnapshot = z.strictObject({ files: rawFileMap, projectVersion: z.string().optional(), v2DocVersions: rawV2DocVersions.nullish(), timestamp: z.iso.datetime().optional(), }) // mirrors Origin.fromRaw: three restore variants plus a generic kind const rawOrigin = z.union([ z.strictObject({ kind: z.literal('restore'), version: z.number().int(), timestamp: z.iso.datetime(), }), z.strictObject({ kind: z.literal('file-restore'), version: z.number().int(), path: z.string(), timestamp: z.iso.datetime(), }), z.strictObject({ kind: z.literal('project-restore'), version: z.number().int(), timestamp: z.iso.datetime(), }), z.strictObject({ kind: z.string(), }), ]) // mirrors Operation.fromRaw: add file / edit file / move (or delete) file / // set file metadata / no-op const rawAddFileOperation = z.strictObject({ pathname: z.string(), file: rawFile, }) const rawMoveFileOperation = z.strictObject({ pathname: z.string(), newPathname: z.string(), }) const rawEditFileOperation = z.union([ rawTextOperation.extend({ pathname: z.string() }), rawAddCommentOperation.extend({ pathname: z.string() }), rawDeleteCommentOperation.extend({ pathname: z.string() }), rawSetCommentStateOperation.extend({ pathname: z.string() }), rawEditNoOperation.extend({ pathname: z.string() }), ]) const rawSetFileMetadataOperation = z.strictObject({ pathname: z.string(), metadata: rawFileMetadata, }) const rawNoOperation = z.strictObject({}) const rawOperation = z.union([ rawAddFileOperation, rawEditFileOperation, rawMoveFileOperation, rawSetFileMetadataOperation, rawNoOperation, ]) const rawChange = z.strictObject({ operations: z.array(rawOperation), timestamp: z.iso.datetime(), // null represents an anonymous author authors: z.array(z.number().int().nullable()).optional(), v2Authors: z.array(zz.objectId().nullable()).optional(), origin: rawOrigin.optional(), projectVersion: z.string().optional(), v2DocVersions: rawV2DocVersions.optional(), }) module.exports = { rawTrackingProps, rawClearTrackingProps, rawInsertOp, rawRemoveOp, rawRetainOp, rawScanOp, rawTextOperation, rawRange, rawAddCommentOperation, rawSetCommentStateOperation, rawDeleteCommentOperation, rawEditNoOperation, rawEditOperation, rawComment, rawTrackedChange, rawStringFileData, rawLinkedFileData, rawBlobHash, rawFileMetadata, rawHashFileData, rawBinaryFileData, rawLazyStringFileData, rawHollowBinaryFileData, rawHollowStringFileData, rawFileData, rawFile, rawFileMap, rawV2DocVersions, rawSnapshot, rawOrigin, rawAddFileOperation, rawMoveFileOperation, rawEditFileOperation, rawSetFileMetadataOperation, rawNoOperation, rawOperation, rawChange, }