Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setMetadata() method merges existing with new data? #597

Closed
jgeewax opened this issue May 13, 2015 · 29 comments · Fixed by #607
Closed

setMetadata() method merges existing with new data? #597

jgeewax opened this issue May 13, 2015 · 29 comments · Fixed by #607
Assignees
Labels
api: storage Issues related to the Cloud Storage API. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. type: question Request for information or clarification. Not an issue.

Comments

@jgeewax
Copy link
Contributor

jgeewax commented May 13, 2015

Looks like if I call setMetadata on an object, it merges the data with what's in production...

var bucket = ...; // Get a bucket somehow.
var file = bucket.file('my-file');

file.setMetadata({
  metadata: {
    first: 'first'
  }
});

file.setMetadata({
  metadata: {
    second: 'second'
  }
}, function(err, metadata) {
  // metadata here is {first: 'first', second: 'second'}
  // Expected it to be just {second: 'second'} ....
});

I suspect that we can't .. fix this all that much... but maybe we can at least document it ?


The options we have so far:

  • Make metadata immutable: remove setMetadata
  • Operate on a single attribute at a time: getMetadata(key) and setMetadata(key, value)
  • Rename setMetadata to patchMetadata or mergeMetadata
  • Add some documentation about the behavior of setMetadata

/cc @Capstan @aozarov

@jgeewax jgeewax added type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. api: storage Issues related to the Cloud Storage API. labels May 13, 2015
@jgeewax jgeewax added this to the Storage Stable milestone May 13, 2015
@jgeewax jgeewax added the type: question Request for information or clarification. Not an issue. label May 13, 2015
@ryanseys
Copy link
Contributor

Remember #459? Yeah we switched to PATCH because using PUT destroyed ACLs with BigQuery, making our datasets inaccessible. I'm not sure if this is also the case with Storage but I feel it would be appropriate to remain consistent across our APIs in this library.

@ryanseys
Copy link
Contributor

We can still document it for our users. If they want to delete an existing value, I believe the correct way is to set it to null.

@aozarov
Copy link

aozarov commented May 13, 2015

Yes, but I doubt if doing it (delete and set) should be done by the library as it can fail in the middle and leave it in an undesired state.

BTW, nulls may be a separate issue which I mentioned to @jgeewax.
In Java nulls are considered as "ignore" in patch operation and instead a special "null" value should be used (e.g. http://javadoc.google-http-java-client.googlecode.com/hg/latest/com/google/api/client/util/Data.html#NULL_BIG_DECIMAL) [which clearly should be done transparently by the library]. Not sure if the same problem applies to node.js apiary client.

@ryanseys
Copy link
Contributor

I doubt if doing it (delete and set) should be done by the library as it can fail in the middle and leave it in an undesired state.

The API call operation is atomic so either the whole thing succeeds or fails.

@ryanseys
Copy link
Contributor

I think I see what you were saying. I meant that using PATCH you can delete existing values by setting them to null. We never make 2 calls for setting metadata. That requires you to know about the ones are set already, which is a getMetadata call away. We leave this as an exercise for the user if they want to do that.

@aozarov
Copy link

aozarov commented May 13, 2015

What I was saying is that we should probably not do the following:

func setMetadata(blob, metadata) {
  rpc.patch(blob, {"metadata": null})
  rpc.patch(blob, {"metadata": metadata})
}

@ryanseys
Copy link
Contributor

Oh we'd never do that. The end result is the same as just rpc.patch(blob, {"metadata": metadata})

I meant if you wanted to set completely new metadata, including destroying the ones currently set, you need to know the existing ones and set them to null so as to achieve the same result as using PUT.

@ryanseys
Copy link
Contributor

Like this:

// metadata is currently { a: '1', b: '2', c: '3' };
rpc.patch(blob, { metadata: { a: 'x', b: null, c: null } }) // need to know b and c exist so you can set them to null
// metadata is now { a: 'x' };

This is the same as:

// metadata is currently { a: '1', b: '2', c: '3' };
rpc.put(blob, { metadata: { a: 'x' } })
// metadata is now { a: 'x' };

@aozarov
Copy link

aozarov commented May 13, 2015

Is it? Is rpc.put doing an update (https://cloud.google.com/storage/docs/json_api/v1/objects/update)?
If so, wouldn't it clear out the other Blob's user-given properties (e.g. contentLanguage)?

@jgeewax
Copy link
Contributor Author

jgeewax commented May 13, 2015

So back to the main issue:

My concern here is that set typically means "replace the value up there with the value I'm sending from down here". set to us means... "merge this value from down here into whatever data exists up there."

Can we rename anything ? or are we just OK with adding some docs that explain this? Or do we try to get Google to fix something ? What makes the most sense as a user of this library?

@aozarov
Copy link

aozarov commented May 13, 2015

I doubt that a function that can only append is that useful.
One options (until fixed by the backed) would be to make it a read-only property which can only be set when creating a new file. Once apiary fix the issue property can become again read-write.

@jgeewax
Copy link
Contributor Author

jgeewax commented May 13, 2015

As a side comment: Metadata (and other properties) on S3 Objects are immutable. To change them you must copy the object.

Another option is to make Metadata not an "object" in Javascript, but instead a method that has keys and values. So:

file.setMetadata('key', 'value', function(metadata) {});

The idea being, you can always clear out the metadata for any key you know of... (.setMetadata('key', null)) and since we don't let you do an entire dictionary, it's never a concern that it operates this way....

If we left the existing setMetadata method, it could be patchMetadata or mergeMetadata instead to clarify what exactly is happening.

Thoughts?

@ryanseys
Copy link
Contributor

I'm against patchMetadata and mergeMetadata because they sound technical in nature and scary. If we change to update semantics then the user must always specify their acls and contentType in the request according to https://cloud.google.com/storage/docs/json_api/v1/objects/update

I doubt that a function that can only append is that useful.

The function is not append only, if you know the key you can set it to null and it will be deleted.

I wish that acls and contentType weren't overwritten when you set the metadata or that the raw key/value pair of custom metadata was completely separate from the "properties" of the object, but this is an issue I have with the API itself, not our library. Not sure how much easier we could make it without making the user jump through more hoops.

@jgeewax
Copy link
Contributor Author

jgeewax commented May 14, 2015

because they sound technical in nature and scary

Because they are -- I'm proposing we make setMetadata an option on a single key, so the catalog here looks like:

// Set a single key of metadata:
file.setMetadata('client', 1234);

// Remove a single key of metadata:
file.setMetadata('client', null);
// or
file.clearMetadata('client');
// or
file.removeMetadata('client');

// Merge in (or patch) a whole bunch of metadata:
file.patchMetadata({  // or mergeMetadata
  important: 'true',
  client: '1234'
});

// Clear all metadata
file.clearMetadata();  // I don't think we have a way to make this atomic...

The idea would be that we're discouraging people from sending in a big dictionary, because it really isn't what you expect. For all other set methods, the action is "replace". If we leave setMetadata as is, it's only "replace" if operating on a single key. It's "merge" or "patch" if operating on the whole thing.

The function is not append only, if you know the key you can set it to null and it will be deleted.

Exactly -- if you know the keys... which may be a long list...

@ryanseys
Copy link
Contributor

The idea would be that we're discouraging people from sending in a big dictionary

So we're encouraging them to make many tiny requests to set all their metadata fields? That's just silly.

I'm more for calling it patchMetadata than I am for dumbing the whole thing down into setting and deleting a single key at a time. The whole idea of the library was to be more idiomatic so we're taking a step backward here by renaming it to patch. Not everyone knows PATCH semantics and we have assumed our developers don't wanna destroy their acls every time (or set them every time either). This was the best trade-off.

If anything we should rename the method and force people to update their code.

file.clearMetadata(); // I don't think we have a way to make this atomic...

Yeah we could use UPDATE semantics for that and just not set anything but I don't know why someone would ever want to do this because their acls will be destroyed, effectively locking them out of updating their file ever again.

@jgeewax
Copy link
Contributor Author

jgeewax commented May 14, 2015

The whole idea of the library was to be more idiomatic so we're taking a step backward here by renaming it to patch.

There is a balance we have to find. We can use words people know, and have them be lies, or we can use words they don't know and have them be the truth. Right now, setMetadata doesn't set data the way someone would expect set to work:

thing.setMetadata({first: 1});
assert(thing.getMetadata() == {first: 1});

thing.setMetadata({second: 2});
assert(thing.getMetadata() == {second: 2}); // This *fails*.

If we make it operate on single values, then it acts like I'd expect set to act:

thing.setMetadata('first', 1);
assert(thing.getMetadata('first') == 1);

thing.setMetadata('second', 2);
assert(thing.getMetadata('second') == 2); // This *passes*.

Regarding clearMetadata: I think that's a mistake too.

Yeah we could use UPDATE semantics for that

We couldn't though, right? If we did that it'd blast out more than just the metadata -- it'd remove everything else (ACLs, etc).

file.makePublic();
assert(/* file is public */);

file.setMetadata({first: 1});
assert(file.getMetadata() == {first: 1});

file.clearMetadata();
assert(file.getMetadata() == null /* or {} */); // This *passes*. 
assert(/* file is public */); // This *fails*, right?

@stephenplusplus
Copy link
Contributor

setThing(value) definitely means "set thing to value, overwriting if necessary".

But I see setMetadata as a complex version of that, given the concept of metadata being an object of "things". So, I see setMetadata() as setMetadata({ thing: this }).

It would be like if we went the road of setMetadata('thing', 'value') and then made a batch method that grouped them together for developer convenience, we would still call it setMetadata, but it would accept an object of things... which would leave us where we are now.

If we're just trying to not confuse developers by our terminology as opposed to adding new functionality, can we each ask a non-Google dev what they think and take a vote? :)

@jgeewax
Copy link
Contributor Author

jgeewax commented May 14, 2015

I'm down with that. How do we find impartial people ? I'm asking around inside Google UX (who do studies and whatnot...) but I suspect you guys might have some people we could poll...

@stephenplusplus
Copy link
Contributor

Sounds good, the more thoughts the better. I'm asking a couple JS friends.

@aozarov
Copy link

aozarov commented May 14, 2015

Did you consider my original suggestion (which apparently is consistent with S3). Only accept metadata in create request and make it a read-only attribute (until the underlying issue is fixed) or do you see it as a problem (conceptually or implementation wise)?

@jgeewax
Copy link
Contributor Author

jgeewax commented May 14, 2015

To be clear, that's a further option, removing setMetadata all together -- and effectively making Metadata immutable from our API.

I suspect people would be a bit miffed at that...

@jgeewax
Copy link
Contributor Author

jgeewax commented May 19, 2015

@stephenplusplus : Any progress on asking around? I'm still seeing how we would do broader user polling on our side.

@stephenplusplus
Copy link
Contributor

I'll follow up with some folks and report back.

I was reminded that React uses setState in a similar way (3rd example from https://facebook.github.io/react/):

screen shot 2015-05-19 at 10 20 45 am

You can see in getInitialState, the "state" object is set to its default value with 2 properties, items and text. Later, in onChange, just the text property is updated, where in handleSubmit, both are updated.

@jgeewax
Copy link
Contributor Author

jgeewax commented May 19, 2015

To be more explicit: setState ... "Merges nextState with the current state."

https://facebook.github.io/react/docs/component-api.html#setstate

@jgeewax
Copy link
Contributor Author

jgeewax commented May 19, 2015

Sounds like the resolution to this issue, then, is to update setMetadata's documentation to say "Merge nextMetadata with the current metadata" or similar.

@ryanseys
Copy link
Contributor

I like that idea :) Just clearer documentation!

@jgeewax
Copy link
Contributor Author

jgeewax commented May 19, 2015 via email

@ryanseys
Copy link
Contributor

What if React does it because someone else did it for no good reason :)

@jgeewax
Copy link
Contributor Author

jgeewax commented May 19, 2015

Then we're just blind followers jumping off cliffs because our friends do :(

That said, I think it's more like jumping into a puddle than off a cliff -- at worst it's slightly surprising for users. Let's make sure we have a good example of what happens when you just "set" new metadata... (that the old data doesn't go away unless you specifically tell it to).

@ryanseys ryanseys self-assigned this May 19, 2015
chingor13 pushed a commit that referenced this issue Aug 22, 2022
- [ ] Regenerate this pull request now.

Committer: @summer-ji-eng
PiperOrigin-RevId: 424244721

Source-Link: googleapis/googleapis@4b6b01f

Source-Link: googleapis/googleapis-gen@8ac83fb
Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiOGFjODNmYmE2MDZkMDA4YzdlOGE0MmU3ZDU1YjY1OTZlYzRiZTM1ZiJ9
chingor13 pushed a commit that referenced this issue Aug 22, 2022
- [ ] Regenerate this pull request now.

Committer: @summer-ji-eng
PiperOrigin-RevId: 424244721

Source-Link: googleapis/googleapis@4b6b01f

Source-Link: googleapis/googleapis-gen@8ac83fb
Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiOGFjODNmYmE2MDZkMDA4YzdlOGE0MmU3ZDU1YjY1OTZlYzRiZTM1ZiJ9
chingor13 pushed a commit that referenced this issue Aug 22, 2022
- [ ] Regenerate this pull request now.

Committer: @summer-ji-eng
PiperOrigin-RevId: 424244721

Source-Link: googleapis/googleapis@4b6b01f

Source-Link: googleapis/googleapis-gen@8ac83fb
Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiOGFjODNmYmE2MDZkMDA4YzdlOGE0MmU3ZDU1YjY1OTZlYzRiZTM1ZiJ9
sofisl pushed a commit that referenced this issue Nov 11, 2022
fix: update protos

Source-Link: googleapis/synthtool@0a68e56
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:5b05f26103855c3a15433141389c478d1d3fe088fb5d4e3217c4793f6b3f245e

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
sofisl pushed a commit that referenced this issue Nov 11, 2022
[![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [sinon](https://sinonjs.org/) ([source](https://github.com/sinonjs/sinon)) | [`^12.0.0` -> `^13.0.0`](https://renovatebot.com/diffs/npm/sinon/12.0.1/13.0.0) | [![age](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/compatibility-slim/12.0.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/sinon/13.0.0/confidence-slim/12.0.1)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>sinonjs/sinon</summary>

### [`v13.0.0`](https://github.com/sinonjs/sinon/blob/HEAD/CHANGES.md#&#8203;1300)

[Compare Source](https://github.com/sinonjs/sinon/compare/v12.0.1...v13.0.0)

-   [`cf3d6c0c`](https://github.com/sinonjs/sinon/commit/cf3d6c0cd9689c0ee673b3daa8bf9abd70304392)
    Upgrade packages ([#&#8203;2431](https://github.com/sinonjs/sinon/issues/2431)) (Carl-Erik Kopseng)
    > -   Update all @&#8203;sinonjs/ packages
    >
    > -   Upgrade to fake-timers 9
    >
    > -   chore: ensure always using latest LTS release
-   [`41710467`](https://github.com/sinonjs/sinon/commit/417104670d575e96a1b645ea40ce763afa76fb1b)
    Adjust deploy scripts to archive old releases in a separate branch, move existing releases out of master ([#&#8203;2426](https://github.com/sinonjs/sinon/issues/2426)) (Joel Bradshaw)
    > Co-authored-by: Carl-Erik Kopseng <carlerik@gmail.com>
-   [`c80a7266`](https://github.com/sinonjs/sinon/commit/c80a72660e89d88b08275eff1028ecb9e26fd8e9)
    Bump node-fetch from 2.6.1 to 2.6.7 ([#&#8203;2430](https://github.com/sinonjs/sinon/issues/2430)) (dependabot\[bot])
    > Co-authored-by: dependabot\[bot] <49699333+dependabot\[bot][@&#8203;users](https://github.com/users).noreply.git.luolix.top>
-   [`a00f14a9`](https://github.com/sinonjs/sinon/commit/a00f14a97dbe8c65afa89674e16ad73fc7d2fdc0)
    Add explicit export for `./*` ([#&#8203;2413](https://github.com/sinonjs/sinon/issues/2413)) (なつき)
-   [`b82ca7ad`](https://github.com/sinonjs/sinon/commit/b82ca7ad9b1add59007771f65a18ee34415de8ca)
    Bump cached-path-relative from 1.0.2 to 1.1.0 ([#&#8203;2428](https://github.com/sinonjs/sinon/issues/2428)) (dependabot\[bot])
-   [`a9ea1427`](https://github.com/sinonjs/sinon/commit/a9ea142716c094ef3c432ecc4089f8207b8dd8b6)
    Add documentation for assert.calledOnceWithMatch ([#&#8203;2424](https://github.com/sinonjs/sinon/issues/2424)) (Mathias Schreck)
-   [`1d5ab86b`](https://github.com/sinonjs/sinon/commit/1d5ab86ba60e50dd69593ffed2bffd4b8faa0d38)
    Be more general in stripping off stack frames to fix Firefox tests ([#&#8203;2425](https://github.com/sinonjs/sinon/issues/2425)) (Joel Bradshaw)
-   [`56b06129`](https://github.com/sinonjs/sinon/commit/56b06129e223eae690265c37b1113067e2b31bdc)
    Check call count type ([#&#8203;2410](https://github.com/sinonjs/sinon/issues/2410)) (Joel Bradshaw)
-   [`7863e2df`](https://github.com/sinonjs/sinon/commit/7863e2dfdbda79e0a32e42af09e6539fc2f2b80f)
    Fix [#&#8203;2414](https://github.com/sinonjs/sinon/issues/2414): make Sinon available on homepage (Carl-Erik Kopseng)
-   [`fabaabdd`](https://github.com/sinonjs/sinon/commit/fabaabdda82f39a7f5b75b55bd56cf77b1cd4a8f)
    Bump nokogiri from 1.11.4 to 1.13.1 ([#&#8203;2423](https://github.com/sinonjs/sinon/issues/2423)) (dependabot\[bot])
-   [`dbc0fbd2`](https://github.com/sinonjs/sinon/commit/dbc0fbd263c8419fa47f9c3b20cf47890a242d21)
    Bump shelljs from 0.8.4 to 0.8.5 ([#&#8203;2422](https://github.com/sinonjs/sinon/issues/2422)) (dependabot\[bot])
-   [`fb8b3d72`](https://github.com/sinonjs/sinon/commit/fb8b3d72a85dc8fb0547f859baf3f03a22a039f7)
    Run Prettier (Carl-Erik Kopseng)
-   [`12a45939`](https://github.com/sinonjs/sinon/commit/12a45939e9b047b6d3663fe55f2eb383ec63c4e1)
    Fix 2377: Throw error when trying to stub non-configurable or non-writable properties ([#&#8203;2417](https://github.com/sinonjs/sinon/issues/2417)) (Stuart Dotson)
    > Fixes issue [#&#8203;2377](https://github.com/sinonjs/sinon/issues/2377) by throwing an error when trying to stub non-configurable or non-writable properties

*Released by [Carl-Erik Kopseng](https://github.com/fatso83) on 2022-01-28.*

</details>

---

### Configuration

📅 **Schedule**: "after 9am and before 3pm" (UTC).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/nodejs-monitoring).
sofisl pushed a commit that referenced this issue Nov 11, 2022
- [ ] Regenerate this pull request now.

Committer: @summer-ji-eng
PiperOrigin-RevId: 424244721

Source-Link: googleapis/googleapis@4b6b01f

Source-Link: googleapis/googleapis-gen@8ac83fb
Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiOGFjODNmYmE2MDZkMDA4YzdlOGE0MmU3ZDU1YjY1OTZlYzRiZTM1ZiJ9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: storage Issues related to the Cloud Storage API. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. type: question Request for information or clarification. Not an issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants