From 725b9725908488cb7497f6d1d8d238f64bc17c25 Mon Sep 17 00:00:00 2001 From: David Bosschaert Date: Fri, 3 May 2024 11:28:48 +0200 Subject: [PATCH] GH-6 Version Labels (#38) Support passing label on version POST/PUT --- src/handlers/post.js | 2 +- src/routes/source.js | 2 +- src/routes/version.js | 4 +- src/storage/version/list.js | 4 +- src/storage/version/patch.js | 4 +- src/storage/version/put.js | 20 ++- test/storage/version/patch.test.js | 12 +- test/storage/version/put.test.js | 188 +++++++++++++++++++++++++++++ 8 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 test/storage/version/put.test.js diff --git a/src/handlers/post.js b/src/handlers/post.js index c1be278..d60f4fd 100644 --- a/src/handlers/post.js +++ b/src/handlers/post.js @@ -20,7 +20,7 @@ export default async function postHandler({ req, env, daCtx }) { if (path.startsWith('/source')) return postSource({ req, env, daCtx }); if (path.startsWith('/config')) return postConfig({ req, env, daCtx }); - if (path.startsWith('/versionsource')) return postVersionSource({ env, daCtx }); + if (path.startsWith('/versionsource')) return postVersionSource({ req, env, daCtx }); if (path.startsWith('/copy')) return copyHandler({ req, env, daCtx }); if (path.startsWith('/rename')) return renameHandler({ req, env, daCtx }); diff --git a/src/routes/source.js b/src/routes/source.js index b974a28..583dd1a 100644 --- a/src/routes/source.js +++ b/src/routes/source.js @@ -35,7 +35,7 @@ async function invalidateCollab(api, url, env) { } export async function deleteSource({ req, env, daCtx }) { - await postObjectVersion(env, daCtx); + await postObjectVersion(req, env, daCtx); const resp = await deleteObject(env, daCtx); if (resp.status === 204) { diff --git a/src/routes/version.js b/src/routes/version.js index d9dbaaf..2a7ba5c 100644 --- a/src/routes/version.js +++ b/src/routes/version.js @@ -27,6 +27,6 @@ export async function patchVersion({ req, env, daCtx }) { return patchObjectVersion(req, env, daCtx); } -export async function postVersionSource({ env, daCtx }) { - return postObjectVersion(env, daCtx); +export async function postVersionSource({ req, env, daCtx }) { + return postObjectVersion(req, env, daCtx); } diff --git a/src/storage/version/list.js b/src/storage/version/list.js index 4937fad..c495ef5 100644 --- a/src/storage/version/list.js +++ b/src/storage/version/list.js @@ -25,7 +25,7 @@ export async function listObjectVersions(env, { org, key }) { }, true); const timestamp = parseInt(entryResp.metadata.timestamp || '0', 10); const users = JSON.parse(entryResp.metadata.users || '[{"email":"anonymous"}]'); - const { displayname, path } = entryResp.metadata; + const { label, path } = entryResp.metadata; if (entryResp.contentLength > 0) { return { @@ -33,7 +33,7 @@ export async function listObjectVersions(env, { org, key }) { users, timestamp, path, - displayname, + label, }; } return { users, timestamp, path }; diff --git a/src/storage/version/patch.js b/src/storage/version/patch.js index 62d221d..1dd1d27 100644 --- a/src/storage/version/patch.js +++ b/src/storage/version/patch.js @@ -22,7 +22,7 @@ function buildInput({ }; } -// Currently only patches the display name into the version +// Currently only patches the label into the version export async function patchObjectVersion(req, env, daCtx) { const rb = await req.json(); const { org, key } = daCtx; @@ -43,7 +43,7 @@ export async function patchObjectVersion(req, env, daCtx) { Users: current.metadata?.users || JSON.stringify([{ email: 'anonymous' }]), Timestamp: current.metadata?.timestamp || `${Date.now()}`, Path: current.metadata?.path || daCtx.key, - Displayname: rb.displayname, + Label: rb.label || current.metadata?.label, }, }, false); return { status: resp.status }; diff --git a/src/storage/version/put.js b/src/storage/version/put.js index 96c9cfb..9c8f285 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -45,13 +45,30 @@ function buildInput({ }; } -export async function postObjectVersion(env, daCtx) { +export async function postObjectVersion(req, env, daCtx) { + let reqJSON; + try { + reqJSON = await req.json(); + } catch (e) { + // no label + } + const config = getS3Config(env); const update = buildInput(daCtx); const current = await getObject(env, daCtx); if (current.status === 404 || !current.metadata?.id || !current.metadata?.version) { return 404; } + + let existingVersion; + if (reqJSON?.label === undefined) { + existingVersion = await getObject(env, { + org: daCtx.org, + key: `.da-versions/${current.metadata.id}/${current.metadata.version}.${daCtx.ext}`, + }); + } + const label = reqJSON?.label || existingVersion?.metadata?.label; + const resp = await putVersion(config, { Bucket: update.Bucket, Body: current.body, @@ -62,6 +79,7 @@ export async function postObjectVersion(env, daCtx) { Users: current.metadata?.users || JSON.stringify([{ email: 'anonymous' }]), Timestamp: current.metadata?.timestamp || `${Date.now()}`, Path: current.metadata?.path || daCtx.key, + Label: label, }, }, false); return { status: resp.status === 200 ? 201 : resp.status }; diff --git a/test/storage/version/patch.test.js b/test/storage/version/patch.test.js index bf4efc2..1fd136a 100644 --- a/test/storage/version/patch.test.js +++ b/test/storage/version/patch.test.js @@ -14,7 +14,7 @@ import esmock from 'esmock'; describe('Version Patch', () => { it('Patch Object unknown', async () => { - const req = { json: async () => JSON.parse('{"displayname": "Some version"}')}; + const req = { json: async () => JSON.parse('{"label": "Some version"}')}; const { patchObjectVersion } = await esmock( '../../../src/storage/version/patch.js', { '../../../src/storage/object/get.js': { @@ -32,7 +32,7 @@ describe('Version Patch', () => { }) it('Patch Object Version', async () => { - const req = { json: async () => JSON.parse('{"displayname": "Some version"}')}; + const req = { json: async () => JSON.parse('{}')}; const env = { S3_DEF_URL: 's3def', S3_ACCESS_KEY_ID: 'id', @@ -54,7 +54,7 @@ describe('Version Patch', () => { const mockedObject = { body: 'obj body', - metadata: { timestamp: '999' } + metadata: { timestamp: '999', label: 'prev label' } }; const mockGetObject = async (e, c) => { if (e === env @@ -100,12 +100,12 @@ describe('Version Patch', () => { assert.equal('[{"email":"anonymous"}]', de.Metadata.Users); assert.equal('999', de.Metadata.Timestamp); assert.equal('mykey', de.Metadata.Path); - assert.equal('Some version', de.Metadata.Displayname); + assert.equal('prev label', de.Metadata.Label); assert.equal(200, resp.status); }); it('Patch Object Version 2', async () => { - const req = { json: async () => JSON.parse('{"displayname": "v999"}')}; + const req = { json: async () => JSON.parse('{"label": "v999"}')}; const env = {}; const daCtx = { org: 'org2', @@ -167,7 +167,7 @@ describe('Version Patch', () => { assert.equal('[{"email":"jbloggs@acme"},{"email":"ablah@halba"}]', de.Metadata.Users); assert.equal('12345', de.Metadata.Timestamp); assert.equal('goobar', de.Metadata.Path); - assert.equal('v999', de.Metadata.Displayname); + assert.equal('v999', de.Metadata.Label); assert.equal(200, resp.status); }); }); \ No newline at end of file diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js new file mode 100644 index 0000000..a6ca820 --- /dev/null +++ b/test/storage/version/put.test.js @@ -0,0 +1,188 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import assert from 'assert'; +import esmock from 'esmock'; + +describe('Version Put', () => { + it('Post Object Version', async () => { + const mockGetObject = async () => { + const metadata = { + id: 'id', + version: '123' + } + return { metadata }; + }; + + const sentToS3 = []; + const s3Client = { + send: async (c) => { + sentToS3.push(c); + return { + $metadata: { + httpStatusCode: 200 + } + }; + } + }; + const mockS3Client = () => s3Client; + + const { postObjectVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + createBucketIfMissing: mockS3Client + }, + }); + + const dn = { label: 'my label' }; + const req = { + json: async () => dn + }; + const env = {}; + const daCtx = { + org: 'myorg', + key: '/a/b/c', + ext: 'html' + }; + + const resp = await postObjectVersion(req, env, daCtx); + assert.equal(201, resp.status); + + assert.equal(1, sentToS3.length); + const input = sentToS3[0].input; + assert.equal('myorg-content', input.Bucket); + assert.equal('.da-versions/id/123.html', input.Key); + assert.equal('[{"email":"anonymous"}]', input.Metadata.Users); + assert.equal('my label', input.Metadata.Label); + assert(input.Metadata.Timestamp > (Date.now() - 2000)); // Less than 2 seconds old + assert.equal('/a/b/c', input.Metadata.Path); + }); + + it('Post Object Version 2', async () => { + const mockGetObject = async () => { + const metadata = { + label: 'old label', + id: 'idx', + version: '456', + path: '/y/z', + timestamp: 999, + users: '[{"email":"foo@acme.org"}]', + } + return { metadata }; + }; + + const sentToS3 = []; + const s3Client = { + send: async (c) => { + sentToS3.push(c); + return { + $metadata: { + httpStatusCode: 202 + } + }; + } + }; + const mockS3Client = () => s3Client; + + const { postObjectVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + createBucketIfMissing: mockS3Client + }, + }); + + const dn = { label: 'my label' }; + const req = {}; + const env = {}; + const daCtx = { + org: 'someorg', + key: '/a/b/c', + ext: 'html' + }; + + const resp = await postObjectVersion(req, env, daCtx); + assert.equal(202, resp.status); + + assert.equal(1, sentToS3.length); + const input = sentToS3[0].input; + assert.equal('someorg-content', input.Bucket); + assert.equal('.da-versions/idx/456.html', input.Key); + assert.equal('[{"email":"foo@acme.org"}]', input.Metadata.Users); + assert.equal('old label', input.Metadata.Label); + assert.equal(999, input.Metadata.Timestamp); + assert.equal('/y/z', input.Metadata.Path); + }); + + it('Post Object Version where Label already exists', async () => { + const mockGetObject = async (e, x) => { + if (x.key === '.da-versions/idx/456.myext') { + const mdver = { + label: 'existing label', + }; + return { metadata: mdver }; + } + const metadata = { + id: 'idx', + version: '456', + path: '/y/z', + timestamp: 999, + users: '[{"email":"one@acme.org"},{"email":"two@acme.org"}]', + } + return { metadata }; + }; + + const sentToS3 = []; + const s3Client = { + send: async (c) => { + sentToS3.push(c); + return { + $metadata: { + httpStatusCode: 200 + } + }; + } + }; + const mockS3Client = () => s3Client; + + const { postObjectVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + createBucketIfMissing: mockS3Client + }, + }); + + const dn = { label: 'my label' }; + const req = {}; + const env = {}; + const daCtx = { + org: 'someorg', + key: '/a/b/c', + ext: 'myext' + }; + + const resp = await postObjectVersion(req, env, daCtx); + assert.equal(201, resp.status); + + assert.equal(1, sentToS3.length); + const input = sentToS3[0].input; + assert.equal('someorg-content', input.Bucket); + assert.equal('.da-versions/idx/456.myext', input.Key); + assert.equal('[{"email":"one@acme.org"},{"email":"two@acme.org"}]', input.Metadata.Users); + assert.equal('existing label', input.Metadata.Label); + assert.equal('/y/z', input.Metadata.Path); + }); +});