From 3fc37253daf13039436e5add11b902cee9716fa9 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 17 Nov 2025 16:16:38 +0100 Subject: [PATCH 1/2] feat: get rid of the Collab Parse version --- src/storage/version/put.js | 9 +- test/storage/version/put.test.js | 275 +++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 6 deletions(-) diff --git a/src/storage/version/put.js b/src/storage/version/put.js index eee2b31..33c1351 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -112,12 +112,9 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { } } - const pps = current.metadata?.preparsingstore || '0'; - - // Store the body if preparsingstore is not defined, so a once-off store - let storeBody = !body && pps === '0'; - const Preparsingstore = storeBody ? Timestamp : pps; - let Label = storeBody ? 'Collab Parse' : update.label; + const Preparsingstore = current.metadata?.preparsingstore || '0'; + let storeBody = false; + let Label = update.label; if (daCtx.method === 'PUT' && daCtx.ext === 'html' diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index b6fafcd..b8a6474 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -866,4 +866,279 @@ describe('Version Put', () => { assert.strictEqual(putCommand.input.Body, 'test body'); assert.strictEqual(putCommand.input.ContentLength, 9); }); + + it('Test NO Collab Parse version - preparsingstore behavior removed', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const mockGetObject = async () => ({ + status: 200, + body: 'Existing content', + contentLength: 42, + contentType: 'text/html', + etag: 'existing-etag', + metadata: { + id: 'doc-123', + version: 'v1', + timestamp: '1234567890', + users: '[{"email":"user@example.com"}]', + path: 'docs/page.html', + preparsingstore: '0' // No longer triggers special behavior + } + }); + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifMatch: () => mockS3Client, + ifNoneMatch: () => mockS3Client, + }, + }); + + const env = {}; + const daCtx = { + org: 'test-org', + bucket: 'test-bucket', + key: 'docs/page.html', + ext: 'html', + method: 'PUT', + users: [{ email: 'user@example.com' }] + }; + + // Call without body parameter + await putObjectWithVersion(env, daCtx, { + bucket: 'test-bucket', + org: 'test-org', + key: 'docs/page.html', + type: 'text/html' + }); + + // Should have 2 commands: putVersion + putObject + assert.strictEqual(sentCommands.length, 2); + + // First command should be putVersion with empty body (no Collab Parse) + const versionCommand = sentCommands[0]; + assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-123/v1.html'); + assert.strictEqual(versionCommand.input.Body, ''); // Empty - no Collab Parse + assert.ok(versionCommand.input.ContentLength === undefined || versionCommand.input.ContentLength === 0); + + // Second command should be putObject updating the main file + const updateCommand = sentCommands[1]; + assert.strictEqual(updateCommand.input.Key, 'test-org/docs/page.html'); + // Preparsingstore should preserve the existing value + assert.strictEqual(updateCommand.input.Metadata.Preparsingstore, '0'); + }); + + it('Test preparsingstore defaults to 0 when undefined', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const mockGetObject = async () => ({ + status: 200, + body: 'Existing content', + contentLength: 42, + contentType: 'text/html', + etag: 'existing-etag', + metadata: { + id: 'doc-456', + version: 'v2', + timestamp: '1234567890', + users: '[{"email":"user@example.com"}]', + path: 'docs/page2.html' + // preparsingstore is undefined - defaults to '0' + } + }); + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifMatch: () => mockS3Client, + ifNoneMatch: () => mockS3Client, + }, + }); + + const env = {}; + const daCtx = { + org: 'test-org', + bucket: 'test-bucket', + key: 'docs/page2.html', + ext: 'html', + method: 'PUT', + users: [{ email: 'user@example.com' }] + }; + + // Call without body parameter + await putObjectWithVersion(env, daCtx, { + bucket: 'test-bucket', + org: 'test-org', + key: 'docs/page2.html', + type: 'text/html' + }); + + // Should have 2 commands: putVersion + putObject + assert.strictEqual(sentCommands.length, 2); + + // First command should be putVersion with empty body + const versionCommand = sentCommands[0]; + assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-456/v2.html'); + assert.strictEqual(versionCommand.input.Body, ''); // Empty - no body stored + + // Second command - preparsingstore should default to '0' + const updateCommand = sentCommands[1]; + assert.strictEqual(updateCommand.input.Metadata.Preparsingstore, '0'); + }); + + it('Test preparsingstore preserves existing value when set', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const mockGetObject = async () => ({ + status: 200, + body: 'Existing content', + contentLength: 42, + contentType: 'text/html', + etag: 'existing-etag', + metadata: { + id: 'doc-789', + version: 'v3', + timestamp: '1234567890', + users: '[{"email":"user@example.com"}]', + path: 'docs/page3.html', + preparsingstore: '1700000000000' // Already set - should be preserved + } + }); + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifMatch: () => mockS3Client, + ifNoneMatch: () => mockS3Client, + }, + }); + + const env = {}; + const daCtx = { + org: 'test-org', + bucket: 'test-bucket', + key: 'docs/page3.html', + ext: 'html', + method: 'PUT', + users: [{ email: 'user@example.com' }] + }; + + // Call without body parameter + await putObjectWithVersion(env, daCtx, { + bucket: 'test-bucket', + org: 'test-org', + key: 'docs/page3.html', + type: 'text/html' + }); + + // Should have 2 commands: putVersion (with empty body) + putObject + assert.strictEqual(sentCommands.length, 2); + + // First command should be putVersion with empty Body + const versionCommand = sentCommands[0]; + assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-789/v3.html'); + assert.strictEqual(versionCommand.input.Body, ''); // Empty - no body stored + // ContentLength can be undefined or 0 for empty body + assert.ok(versionCommand.input.ContentLength === undefined || versionCommand.input.ContentLength === 0); + + // Second command should preserve the existing preparsingstore value + const updateCommand = sentCommands[1]; + assert.strictEqual(updateCommand.input.Metadata.Preparsingstore, '1700000000000'); + }); + + it('Test version stores body when body parameter is provided', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const mockGetObject = async () => ({ + status: 200, + body: 'Old content', + contentLength: 36, + contentType: 'text/html', + etag: 'existing-etag', + metadata: { + id: 'doc-abc', + version: 'v4', + timestamp: '1234567890', + users: '[{"email":"user@example.com"}]', + path: 'docs/page4.html', + preparsingstore: '0' + } + }); + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifMatch: () => mockS3Client, + ifNoneMatch: () => mockS3Client, + }, + }); + + const env = {}; + const daCtx = { + org: 'test-org', + bucket: 'test-bucket', + key: 'docs/page4.html', + ext: 'html', + method: 'PUT', + users: [{ email: 'user@example.com' }] + }; + + // Call WITH body parameter + await putObjectWithVersion(env, daCtx, { + bucket: 'test-bucket', + org: 'test-org', + key: 'docs/page4.html', + body: 'New content', + type: 'text/html' + }, true); // body parameter is true + + // Should have 2 commands: putVersion + putObject + assert.strictEqual(sentCommands.length, 2); + + // First command should be putVersion with the OLD body content + const versionCommand = sentCommands[0]; + assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-abc/v4.html'); + assert.strictEqual(versionCommand.input.Body, 'Old content'); + assert.strictEqual(versionCommand.input.ContentLength, 36); + }); }); From d5cb0e008301cd4713dbd3501046c209bab0b150 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 17 Nov 2025 16:26:09 +0100 Subject: [PATCH 2/2] feat: no empty version --- src/storage/version/put.js | 39 ++++++------ test/storage/version/put.test.js | 103 ++++++++++++++----------------- 2 files changed, 66 insertions(+), 76 deletions(-) diff --git a/src/storage/version/put.js b/src/storage/version/put.js index 33c1351..c371ccd 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -129,25 +129,28 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { Label = 'Restore Point'; } - const versionResp = await putVersion(config, { - Bucket: input.Bucket, - Org: daCtx.org, - Body: (body || storeBody ? current.body : ''), - ContentLength: (body || storeBody ? current.contentLength : undefined), - ContentType: current.contentType, - ID, - Version, - Ext: daCtx.ext, - Metadata: { - Users: current.metadata?.users || JSON.stringify([{ email: 'anonymous' }]), - Timestamp: current.metadata?.timestamp || Timestamp, - Path: current.metadata?.path || Path, - Label, - }, - }); + // Only create version if we have content to store + if (body || storeBody) { + const versionResp = await putVersion(config, { + Bucket: input.Bucket, + Org: daCtx.org, + Body: current.body, + ContentLength: current.contentLength, + ContentType: current.contentType, + ID, + Version, + Ext: daCtx.ext, + Metadata: { + Users: current.metadata?.users || JSON.stringify([{ email: 'anonymous' }]), + Timestamp: current.metadata?.timestamp || Timestamp, + Path: current.metadata?.path || Path, + Label, + }, + }); - if (versionResp.status !== 200 && versionResp.status !== 412) { - return { status: versionResp.status, metadata: { id: ID } }; + if (versionResp.status !== 200 && versionResp.status !== 412) { + return { status: versionResp.status, metadata: { id: ID } }; + } } const client = ifMatch(config, `${current.etag}`); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index b8a6474..1b65141 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -286,7 +286,7 @@ describe('Version Put', () => { assert(s3Sent[0].input.Metadata.Timestamp > 0); }); - it('Put Object With Version don\'t store content', async () => { + it('Put Object With Version don\'t store content - no version created', async () => { const mockGetObject = async (e, u, h) => { if (!h) { return { @@ -335,14 +335,10 @@ describe('Version Put', () => { const resp = await putObjectWithVersion(env, daCtx, update, false); assert.equal(202, resp.status); assert.equal('q123-456', resp.metadata.id); - assert.equal(1, s3VersionSent.length); - assert.equal('', s3VersionSent[0].input.Body); - assert.equal('bbb', s3VersionSent[0].input.Bucket); - assert.equal('myorg/.da-versions/q123-456/ver123.html', s3VersionSent[0].input.Key); - assert.equal('[{"email":"anonymous"}]', s3VersionSent[0].input.Metadata.Users); - assert.equal('a/x.html', s3VersionSent[0].input.Metadata.Path); - assert(s3VersionSent[0].input.Metadata.Timestamp > 0); + // No version created when body parameter is false + assert.equal(0, s3VersionSent.length); + // Only main file update assert.equal(1, s3Sent.length); assert.equal('new-body', s3Sent[0].input.Body); assert.equal('bbb', s3Sent[0].input.Bucket); @@ -352,7 +348,6 @@ describe('Version Put', () => { assert.equal('[{\"email\":\"foo@acme.org\"},{\"email\":\"bar@acme.org\"}]', s3Sent[0].input.Metadata.Users); assert.notEqual('aaa-bbb', s3Sent[0].input.Metadata.Version); assert(s3Sent[0].input.Metadata.Timestamp > 0); - assert((s3Sent[0].input.Metadata.Preparsingstore - s3Sent[0].input.Metadata.Timestamp) < 100); }); it('Put First Object With Version', async () => { @@ -535,7 +530,7 @@ describe('Version Put', () => { assert(s3INMSent[0].input.Body !== s3Sent[0].input.Body ) }); - it('Test putObjectWithVersion HEAD', async () => { + it('Test putObjectWithVersion HEAD - no version created', async () => { const mockGetObject = async () => { const metadata = { id: 'idabc', @@ -545,13 +540,24 @@ describe('Version Put', () => { users: '[{"email":"anonymous"}]', preparsingstore: 12345, } - return { body: '', metadata, contentLength: 616 }; + return { body: '', metadata, contentLength: 616, status: 200, etag: 'test-etag' }; }; - const sentToS3 = []; - const s3Client = { + const versionSent = []; + const mainFileSent = []; + const versionClient = { send: async (c) => { - sentToS3.push(c); + versionSent.push(c); + return { + $metadata: { + httpStatusCode: 200 + } + }; + } + }; + const mainFileClient = { + send: async (c) => { + mainFileSent.push(c); return { $metadata: { httpStatusCode: 201 @@ -559,25 +565,24 @@ describe('Version Put', () => { }; } }; - const mockS3Client = () => s3Client; + const mockIfNoneMatch = () => versionClient; + const mockIfMatch = () => mainFileClient; const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { '../../../src/storage/object/get.js': { default: mockGetObject }, '../../../src/storage/utils/version.js': { - ifNoneMatch: mockS3Client + ifNoneMatch: mockIfNoneMatch, + ifMatch: mockIfMatch, }, }); const resp = await putObjectWithVersion({}, { method: 'HEAD' }, {}); - assert.equal(1, sentToS3.length); - const input = sentToS3[0].input; - assert.equal('', input.Body, 'Empty body for HEAD'); - assert.equal(0, input.ContentLength, 'Should have used 0 as content length for HEAD'); - assert.equal('/q', input.Metadata.Path); - assert.equal(123, input.Metadata.Timestamp); - assert.equal('[{"email":"anonymous"}]', input.Metadata.Users); + // No version created for HEAD without body parameter + assert.equal(0, versionSent.length, 'No version should be created'); + // Main file update still happens + assert.equal(1, mainFileSent.length, 'Main file should be updated'); }); it('Test putObjectWithVersion BODY', async () => { @@ -867,7 +872,7 @@ describe('Version Put', () => { assert.strictEqual(putCommand.input.ContentLength, 9); }); - it('Test NO Collab Parse version - preparsingstore behavior removed', async () => { + it('Test NO version created when body parameter not provided', async () => { const sentCommands = []; const mockS3Client = { async send(cmd) { @@ -890,7 +895,7 @@ describe('Version Put', () => { timestamp: '1234567890', users: '[{"email":"user@example.com"}]', path: 'docs/page.html', - preparsingstore: '0' // No longer triggers special behavior + preparsingstore: '0' } }); @@ -914,7 +919,7 @@ describe('Version Put', () => { users: [{ email: 'user@example.com' }] }; - // Call without body parameter + // Call without body parameter - no version should be created await putObjectWithVersion(env, daCtx, { bucket: 'test-bucket', org: 'test-org', @@ -922,23 +927,17 @@ describe('Version Put', () => { type: 'text/html' }); - // Should have 2 commands: putVersion + putObject - assert.strictEqual(sentCommands.length, 2); - - // First command should be putVersion with empty body (no Collab Parse) - const versionCommand = sentCommands[0]; - assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-123/v1.html'); - assert.strictEqual(versionCommand.input.Body, ''); // Empty - no Collab Parse - assert.ok(versionCommand.input.ContentLength === undefined || versionCommand.input.ContentLength === 0); + // Should have only 1 command: putObject (no version created) + assert.strictEqual(sentCommands.length, 1); - // Second command should be putObject updating the main file - const updateCommand = sentCommands[1]; + // Only command should be putObject updating the main file + const updateCommand = sentCommands[0]; assert.strictEqual(updateCommand.input.Key, 'test-org/docs/page.html'); // Preparsingstore should preserve the existing value assert.strictEqual(updateCommand.input.Metadata.Preparsingstore, '0'); }); - it('Test preparsingstore defaults to 0 when undefined', async () => { + it('Test preparsingstore defaults to 0 when undefined - no version created', async () => { const sentCommands = []; const mockS3Client = { async send(cmd) { @@ -993,20 +992,15 @@ describe('Version Put', () => { type: 'text/html' }); - // Should have 2 commands: putVersion + putObject - assert.strictEqual(sentCommands.length, 2); - - // First command should be putVersion with empty body - const versionCommand = sentCommands[0]; - assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-456/v2.html'); - assert.strictEqual(versionCommand.input.Body, ''); // Empty - no body stored + // Should have only 1 command: putObject (no version created) + assert.strictEqual(sentCommands.length, 1); - // Second command - preparsingstore should default to '0' - const updateCommand = sentCommands[1]; + // Only command - preparsingstore should default to '0' + const updateCommand = sentCommands[0]; assert.strictEqual(updateCommand.input.Metadata.Preparsingstore, '0'); }); - it('Test preparsingstore preserves existing value when set', async () => { + it('Test preparsingstore preserves existing value when set - no version created', async () => { const sentCommands = []; const mockS3Client = { async send(cmd) { @@ -1061,18 +1055,11 @@ describe('Version Put', () => { type: 'text/html' }); - // Should have 2 commands: putVersion (with empty body) + putObject - assert.strictEqual(sentCommands.length, 2); - - // First command should be putVersion with empty Body - const versionCommand = sentCommands[0]; - assert.strictEqual(versionCommand.input.Key, 'test-org/.da-versions/doc-789/v3.html'); - assert.strictEqual(versionCommand.input.Body, ''); // Empty - no body stored - // ContentLength can be undefined or 0 for empty body - assert.ok(versionCommand.input.ContentLength === undefined || versionCommand.input.ContentLength === 0); + // Should have only 1 command: putObject (no version created) + assert.strictEqual(sentCommands.length, 1); - // Second command should preserve the existing preparsingstore value - const updateCommand = sentCommands[1]; + // Only command should preserve the existing preparsingstore value + const updateCommand = sentCommands[0]; assert.strictEqual(updateCommand.input.Metadata.Preparsingstore, '1700000000000'); });