Quellcode durchsuchen

Merge pull request #32892 from overleaf/mj-history-file-metadata

[history-v1] Add missing properties to zod schemas

GitOrigin-RevId: e16faf56a9b294516152383585493b38685b8d15
Mathias Jakobsen vor 3 Monaten
Ursprung
Commit
4173c98c48

+ 11 - 4
services/history-v1/api/schema.js

@@ -8,21 +8,28 @@ const hexHashPattern = new RegExp(Blob.HEX_HASH_RX_STRING)
 const fileSchema = 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 snapshotSchema = z.object({
-  files: z.record(z.string(), fileSchema),
-})
-
 const v2DocVersionsSchema = z.object({
   pathname: z.string().optional(),
   v: z.number().int().optional(),
 })
 
+const snapshotSchema = z.object({
+  files: z.record(z.string(), fileSchema),
+  projectVersion: z.string().optional(),
+  v2DocVersions: z
+    .record(z.string(), v2DocVersionsSchema)
+    .nullable()
+    .optional(),
+  timestamp: z.string().optional(),
+})
+
 const operationSchema = z
   .object({
     pathname: z.string().optional(),

+ 119 - 0
services/history-v1/test/acceptance/js/api/project_updates.test.js

@@ -22,6 +22,7 @@ const TextOperation = core.TextOperation
 const V2DocVersions = core.V2DocVersions
 
 const knex = require('../../../../storage').knex
+const blobHash = require('../../../../storage/lib/blob_hash')
 
 describe('history import', function () {
   beforeEach(cleanup.everything)
@@ -513,6 +514,124 @@ describe('history import', function () {
       })
   })
 
+  it('preserves rangesHash when importing a file', async function () {
+    const testProjectId = '1'
+    const testFilePathname = 'main.tex'
+    // hello.txt contains "Olá mundo\n" (10 UTF-8 chars).
+    const rangesContent = JSON.stringify({
+      comments: [
+        {
+          id: 'comment-1',
+          ranges: [{ pos: 0, length: 3 }],
+          resolved: false,
+        },
+      ],
+      trackedChanges: [
+        {
+          range: { pos: 4, length: 5 },
+          tracking: {
+            type: 'insert',
+            userId: 'user-1',
+            ts: '2024-01-01T00:00:00.000Z',
+          },
+        },
+      ],
+    })
+    const testRangesHash = blobHash.fromString(rangesContent)
+    const testFile = File.fromHash(testFiles.HELLO_TXT_HASH, testRangesHash)
+
+    const [contentResponse, rangesResponse] = await Promise.all([
+      fetch(
+        testServer.url(
+          `/api/projects/${testProjectId}/blobs/${testFiles.HELLO_TXT_HASH}`
+        ),
+        {
+          method: 'PUT',
+          body: fs.createReadStream(testFiles.path('hello.txt')),
+          headers: { Authorization: testServer.basicAuthHeader },
+        }
+      ),
+      fetch(
+        testServer.url(
+          `/api/projects/${testProjectId}/blobs/${testRangesHash}`
+        ),
+        {
+          method: 'PUT',
+          body: rangesContent,
+          headers: { Authorization: testServer.basicAuthHeader },
+        }
+      ),
+    ])
+    expect(contentResponse.ok).to.be.true
+    expect(rangesResponse.ok).to.be.true
+
+    const testSnapshot = new Snapshot()
+    testSnapshot.addFile(testFilePathname, testFile)
+    const importResponse =
+      await basicAuthClient.apis.ProjectImport.importSnapshot1({
+        project_id: testProjectId,
+        snapshot: testSnapshot.toRaw(),
+      })
+    expect(importResponse.obj.projectId).to.equal(testProjectId)
+
+    const historyResponse =
+      await clientForProject.apis.Project.getLatestHistory({
+        project_id: testProjectId,
+      })
+    const chunk = ChunkResponse.fromRaw(historyResponse.obj).getChunk()
+    const file = chunk.getSnapshot().getFile(testFilePathname)
+    expect(file.getRangesHash()).to.equal(testRangesHash)
+  })
+
+  it('preserves projectVersion, v2DocVersions and timestamp on snapshot import', async function () {
+    const testProjectId = '1'
+    const testFilePathname = 'empty.tex'
+    const testDocId = '000000000000000000000001'
+    const testProjectVersion = '12345.0'
+    const testV2DocVersions = new V2DocVersions({
+      [testDocId]: { pathname: testFilePathname, v: 123 },
+    })
+    const testTimestamp = new Date('2024-01-01T00:00:00.000Z')
+
+    const response = await fetch(
+      testServer.url(
+        `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
+      ),
+      {
+        method: 'PUT',
+        body: fs.createReadStream(testFiles.path('empty.tex')),
+        headers: { Authorization: testServer.basicAuthHeader },
+      }
+    )
+    expect(response.ok).to.be.true
+
+    const testSnapshot = new Snapshot()
+    testSnapshot.addFile(testFilePathname, File.fromHash(File.EMPTY_FILE_HASH))
+    testSnapshot.setProjectVersion(testProjectVersion)
+    testSnapshot.setV2DocVersions(testV2DocVersions)
+    testSnapshot.setTimestamp(testTimestamp)
+
+    const importResponse =
+      await basicAuthClient.apis.ProjectImport.importSnapshot1({
+        project_id: testProjectId,
+        snapshot: testSnapshot.toRaw(),
+      })
+    expect(importResponse.obj.projectId).to.equal(testProjectId)
+
+    const historyResponse =
+      await clientForProject.apis.Project.getLatestHistory({
+        project_id: testProjectId,
+      })
+    const snapshot = ChunkResponse.fromRaw(historyResponse.obj)
+      .getChunk()
+      .getSnapshot()
+    expect(snapshot.getProjectVersion()).to.equal(testProjectVersion)
+    expect(snapshot.getV2DocVersions()).to.deep.equal(testV2DocVersions)
+    expect(snapshot.getTimestamp()?.toISOString()).to.equal(
+      testTimestamp.toISOString()
+    )
+  })
+
   it('rejects text operations on binary files', function () {
     const testProjectId = '1'
     const testFilePathname = 'main.tex'