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

feat(core)!: metadata on records #505

Conversation

berendsliedrecht
Copy link
Contributor

@berendsliedrecht berendsliedrecht commented Oct 26, 2021

BREAKING CHANGE: credentialRecord.credentialMetadata has been replaced by credentialRecord.metadata.

  • Adds metadata to the baserecord
  • Does not do any persistency and should be done manually after updating the metadata no the record.
  • Can be used by framework developers and consumers.

Maintainers: Before merging and squashing make sure to copy the BREAKING CHANGE entry to the commit message field, this is needed for automatic tracking of breaking changes

@codecov-commenter
Copy link

codecov-commenter commented Oct 26, 2021

Codecov Report

Merging #505 (162b7c9) into main (336baa0) will increase coverage by 0.06%.
The diff coverage is 94.87%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #505      +/-   ##
==========================================
+ Coverage   86.42%   86.49%   +0.06%     
==========================================
  Files         266      267       +1     
  Lines        5733     5762      +29     
  Branches      923      932       +9     
==========================================
+ Hits         4955     4984      +29     
  Misses        777      777              
  Partials        1        1              
Impacted Files Coverage Δ
packages/core/src/storage/BaseRecord.ts 83.87% <83.33%> (+8.87%) ⬆️
...modules/credentials/repository/CredentialRecord.ts 92.18% <100.00%> (+0.95%) ⬆️
.../modules/credentials/services/CredentialService.ts 95.74% <100.00%> (-0.04%) ⬇️
packages/core/src/storage/Metadata.ts 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 336baa0...162b7c9. Read the comment docs.

@berendsliedrecht berendsliedrecht marked this pull request as ready for review October 26, 2021 12:12
@berendsliedrecht berendsliedrecht requested a review from a team as a code owner October 26, 2021 12:12
Copy link
Contributor

@TimoGlastra TimoGlastra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good @blu3beri. Only thing I'm not sure about is the checks in the credential record constructor

packages/core/src/storage/Metadata.ts Show resolved Hide resolved
Comment on lines 99 to 113
if (props.metadata) {
if (props.metadata.requestMetadata) {
this.metadata.set('requestMetadata', props.metadata.requestMetadata)
}
if (props.metadata.schemaId) {
this.metadata.add('indyCredentialMetadata', {
schemaId: props.metadata.schemaId,
})
}
if (props.metadata.credentialDefinitionId) {
this.metadata.add('indyCredentialMetadata', {
credentialDefinitionId: props.metadata.credentialDefinitionId,
})
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't reached v0.1.0 yet so maybe it would be easier to just see this as a breaking change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you propose to do this? Some part of the framework (in credentialService.ts) is dependent on these fields.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just let them call the metadata themselves

const record = new CredentialRecord({ /** config props */ })

record.metadata.set('indyCredentialMetadata', {
  schemaId: '',
  credentialDefinitionId: ''
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be wise to increment our version in some manner to indicate a breaking change here, since this could cause fairly significant issues for existing systems? Is there harm in utilizing @blu3beri's original mechanism until a later date?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @blu3beri's approach wouldn't work out of the box. The constructor is not called with properties when a record is retrieved from storage. We could add a custom transformer, but I'd rather only take these things into account between stable releases.

Maybe we can look at writing transform scripts. I think we should be very cautious with adding these if statements to the codebase. I don't want to be stuck with this piece of code for eternity. We can never assume when all records are transformed. We're going to have a lot more breaking changes going forward that I'll rather solve by writing update scripts. I'd day that is a reasonable for pre-1.0 releases

Maybe we can keep it in here till 0.1.0. Then for 0.1.0 we provide scripts, and for 0.2.0 we again provide a script for the transformation.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am misunderstanding your point, but right now we set this data in the constructor already and the credentialService, line 612 and 627, relay on these fields. It will even error out when we do not have these fields in the metadata.

I am okay with leaving this out, but how do you propose we handle that error case?

Copy link
Contributor

@TimoGlastra TimoGlastra Oct 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay we were talking about two different things here. For your point @blu3beri, I suggest we remove the metadata from the constructor and require it to be set from using the record.metadata.set afterwards.

What I was mainly thinking about here is the persistence. The metadata on the current credential records has a different structure than the metadata we're adding in this PR. My thinking was that we add something like this migration script called 0.1.0-alpha.291-0.1.0-alpha.292.ts

export async function migrate291To292(agent: Agent) {
  const credentialRecords = await agent.credentials.getAll();
  const credentialRepository = await agent.injectionContainer.resolve(
    CredentialRepository
  );

  for (const credentialRecord of credentialRecords) {
    const data = credentialRecord.metadata.data;
    const metadata = MetaData();

    if (data.requestMetadata) {
      metadata.set("indyRequestMetadata", data.requestMetadata);
    }
    if (data.schemaId) {
      this.metadata.add("indyCredentialMetadata", {
        schemaId: data.schemaId,
      });
    }
    if (data.credentialDefinitionId) {
      this.metadata.add("indyCredentialMetadata", {
        credentialDefinitionId: data.credentialDefinitionId,
      });
    }

    credentialRecord.metadata = metadata;
    await credentialRepository.update(credentialRecord);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh okay. I am in favour of the migration scripts, but for the 0.1.0 and so on. 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'd be in favor of migration scripts, as that'd be very nice for breaking changes. We could either tackle that now or at a 0.1.0 release, as you mentioned. :)
I'll admit I'm not entirely sure how that should be handled from a mobile app perspective, but that can be figured out. :)

packages/core/src/storage/BaseRecord.ts Outdated Show resolved Hide resolved
packages/core/src/storage/Metadata.ts Outdated Show resolved Hide resolved
Comment on lines 99 to 113
if (props.metadata) {
if (props.metadata.requestMetadata) {
this.metadata.set('requestMetadata', props.metadata.requestMetadata)
}
if (props.metadata.schemaId) {
this.metadata.add('indyCredentialMetadata', {
schemaId: props.metadata.schemaId,
})
}
if (props.metadata.credentialDefinitionId) {
this.metadata.add('indyCredentialMetadata', {
credentialDefinitionId: props.metadata.credentialDefinitionId,
})
}
}
Copy link
Contributor

@TimoGlastra TimoGlastra Oct 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay we were talking about two different things here. For your point @blu3beri, I suggest we remove the metadata from the constructor and require it to be set from using the record.metadata.set afterwards.

What I was mainly thinking about here is the persistence. The metadata on the current credential records has a different structure than the metadata we're adding in this PR. My thinking was that we add something like this migration script called 0.1.0-alpha.291-0.1.0-alpha.292.ts

export async function migrate291To292(agent: Agent) {
  const credentialRecords = await agent.credentials.getAll();
  const credentialRepository = await agent.injectionContainer.resolve(
    CredentialRepository
  );

  for (const credentialRecord of credentialRecords) {
    const data = credentialRecord.metadata.data;
    const metadata = MetaData();

    if (data.requestMetadata) {
      metadata.set("indyRequestMetadata", data.requestMetadata);
    }
    if (data.schemaId) {
      this.metadata.add("indyCredentialMetadata", {
        schemaId: data.schemaId,
      });
    }
    if (data.credentialDefinitionId) {
      this.metadata.add("indyCredentialMetadata", {
        credentialDefinitionId: data.credentialDefinitionId,
      });
    }

    credentialRecord.metadata = metadata;
    await credentialRepository.update(credentialRecord);
  }
}

Signed-off-by: Berend Sliedrecht <[email protected]>
Copy link
Contributor

@TimoGlastra TimoGlastra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@JamesKEbert
Copy link
Contributor

So, just to confirm, this would have a breaking change at this point still, right? I have some cases that would need a way to migrate--so this would be a spot that would need a migration script, right?

@berendsliedrecht
Copy link
Contributor Author

So, just to confirm, this would have a breaking change at this point still, right? I have some cases that would need a way to migrate--so this would be a spot that would need a migration script, right?

Yes, a migration script from all the previous versions to this one is required. @TimoGlastra gave one in the comments above that should, or maybe with some minor tweaking.

Signed-off-by: Berend Sliedrecht <[email protected]>
@TimoGlastra
Copy link
Contributor

Decision made on WG call: Use @Transformer for now to transform the record when it is constructed. When moving to 0.1.0 we will add a migration script.

@blu3beri could you make the required changes?

Signed-off-by: Berend Sliedrecht <[email protected]>
credentialDefinitionId: credDefId,
},
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit confusing that we have metadata.data.indyRequestMetadata? Wouldn't be more straightforward to have metadata.indyRequest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data object is there just for structure. My design was that the user could not easily call metadata.indyRequestMetdata and do it through the get,set, etc. Now, the data does not remove this, but I do think it promotes the usage of the functions to get values.

renaming indyRequestMetdata to indyRequest is okay with me. It does seem a bit too verbose now that I think about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think it would be more concise to use just indyRequest instead of indyRequestMetdata at least.

@jakubkoci
Copy link
Contributor

What if a developer creates some metadata:

credentialRecord.metadata.set('something', { a: 1 })
credentialRecord.metadata.set('something else', { b: 2 })

And then decides to remove all of them:

Object.keys(credentialRecord.metadata.data).forEach(key => credentialRecord.metadata.delete(key))

Is it possible to do so? Would that delete also internal metadata?

@berendsliedrecht
Copy link
Contributor Author

Is it possible to do so? Would that delete also internal metadata?

Yes that is possible and it would also delete the internal metadata.

@jakubkoci
Copy link
Contributor

If I understand correctly, deleting the internal metadata would break the agent, right?

It would be great if we're able to prevent it somehow.

@TimoGlastra
Copy link
Contributor

If I understand correctly, deleting the internal metadata would break the agent, right?

It would be great if we're able to prevent it somehow.

I'd say we prefix it with internal/ if you then delete it that would be weird. Nothing prevents the user from overwriting the state of a record and saving that

@berendsliedrecht
Copy link
Contributor Author

If I understand correctly, deleting the internal metadata would break the agent, right?

It would be great if we're able to prevent it somehow.

I agree with @TimoGlastra here. It will always be possible for a framework consumer to modify the data in a record.

If we would, for instance, throw an error when we try to delete a key-value via record.metadata.delete(key) we could still call the underlying functionality which would not error. e.g. delete metadata.data[key].

I completely understand your point, but next to maybe prefixing it, which I am not even sure we should do, but I don't think we should/can do anything about this.

@jakubkoci
Copy link
Contributor

jakubkoci commented Nov 12, 2021

I agree with @TimoGlastra here. It will always be possible for a framework consumer to modify the data in a record.

That's not so easy to do. I realized it's therefore not so easy to manipulate with a metadata object.
But if the purpose of metadata is (or will be) to offer the users some space where they can store (create, update, delete) their metadata for a credential record, it's dangerous to mix those "custom" metadata and "framework" metadata into the same object.

Copy link
Contributor

@jakubkoci jakubkoci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can merge it, but I still have concerns that mixing custom and framework metadata can cause trouble in the future.

@berendsliedrecht
Copy link
Contributor Author

We can merge it, but I still have concerns that mixing custom and framework metadata can cause trouble in the future.

If you could create an issue for it I will think about the options that are available.

@berendsliedrecht berendsliedrecht merged commit c92393a into openwallet-foundation:main Nov 12, 2021
@TimoGlastra
Copy link
Contributor

We can merge it, but I still have concerns that mixing custom and framework metadata can cause trouble in the future.

@jakubkoci What would your ideal solution be for this? Add two types of metadata to each record (framework and consumer metadata)?

@TimoGlastra
Copy link
Contributor

@blu3beri I think it would be good to prefix internal metadata to avoid collisions. We can then say that you are never allowed to use this prefix yourself. I've seen this pattern used in other libraries. Could you make a follow-up PR @blu3beri (rather quickly cause it's breaking again) to add the prefix? I'd say we prefix it with something like _internal/ (other suggestions also welcome)

@berendsliedrecht
Copy link
Contributor Author

@blu3beri I think it would be good to prefix internal metadata to avoid collisions. We can then say that you are never allowed to use this prefix yourself. I've seen this pattern used in other libraries. Could you make a follow-up PR @blu3beri (rather quickly cause it's breaking again) to add the prefix? I'd say we prefix it with something like _internal/ (other suggestions also welcome)

What would you say about an internal key? like

metadata.set('internal', {foo: 'bar'...})

This way we group ALL of the internal stuff in one object. (This might be considered a downside, that's why I am asking) :)

@TimoGlastra
Copy link
Contributor

Yes that could work, but thinking about v2 protocols supporting multiple credential types I think it may be good to e.g. split it metadata per credential type. But that could of course also be nested under the same key. It would however make updating harder (you can't use .add()`)

@berendsliedrecht
Copy link
Contributor Author

Yes that could work, but thinking about v2 protocols supporting multiple credential types I think it may be good to e.g. split it metadata per credential type. But that could of course also be nested under the same key. It would however make updating harder (you can't use .add()`)

Okay I 10000% agree with the fact that it is harder to update. That was the main reason I did not propose this solution earlier. I will create a new pr with the prefix. I think that _internal is good.

@berendsliedrecht berendsliedrecht deleted the feat/record-metadata branch August 31, 2022 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants