-
Notifications
You must be signed in to change notification settings - Fork 28
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: nested ctype val #919
base: develop
Are you sure you want to change the base?
feat: nested ctype val #919
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, this is bad practice to create a Named
function into another Named
function unless you are using it multiple times.
There is a time and place for doing it, but it isn't here.
If you are worried about the typing you can cast or try another approach.
const references = extractUniqueReferences(cType) | ||
const referencedCTypes: ICType[] = [] | ||
|
||
for (const ref of references) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest using a Promise.all
here to reduce the code complexity and make this more efficient.
Additionally, change this from a for..loop
with the await inside to a map. Using the map
const
you can use the Promise.all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, use array methods here please. E.g., await Promise.all(Array.from(references).map(ctypeId => loadCTypes(ctypeId)))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: 77b9c3d
// Helper function to check if CType is nested | ||
function cTypeTypeFinder(cType: ICType): boolean { | ||
function hasRef(obj: any): boolean { | ||
if (typeof obj !== 'object' || obj === null) return false | ||
|
||
if ('$ref' in obj) return true | ||
|
||
return Object.values(obj).some(value => { | ||
if (Array.isArray(value)) { | ||
return value.some(item => hasRef(item)) | ||
} | ||
if (typeof value === 'object') { | ||
return hasRef(value) | ||
} | ||
return false | ||
}) | ||
} | ||
return hasRef(cType.properties) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to make another Named
function inside this function.
// Helper function to check if CType is nested | |
function cTypeTypeFinder(cType: ICType): boolean { | |
function hasRef(obj: any): boolean { | |
if (typeof obj !== 'object' || obj === null) return false | |
if ('$ref' in obj) return true | |
return Object.values(obj).some(value => { | |
if (Array.isArray(value)) { | |
return value.some(item => hasRef(item)) | |
} | |
if (typeof value === 'object') { | |
return hasRef(value) | |
} | |
return false | |
}) | |
} | |
return hasRef(cType.properties) | |
} | |
// Helper function to check if CType is nested | |
function cTypeTypeFinder(cType: ICType): boolean { | |
if (typeof cType.properties !== 'object' || cType.properties === null) return false | |
if ('$ref' in cType.properties) return true | |
return Object.values(cType.properties).some(value => { | |
if (Array.isArray(value)) { | |
return value.some(item => hasRef(item)) | |
} | |
if (typeof value === 'object') { | |
return hasRef(value) | |
} | |
return false | |
}) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that you're defining a function to call it recursively, but can't you just do that with the outer function directly? Also, more generally, I think all that you need is the second function; if you're already recursing through the CType, collect all the references, we will need them eventually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: 77b9c3d
} | ||
|
||
// Helper function to extract unique references from CType | ||
function extractUniqueReferences(cType: ICType): Set<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above, change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I can somewhat see the point of having an inner function; alternatively, you could solve it by taking an optional second argument:
function extractUniqueReferences(cType: ICType): Set<string> { | |
function extractUniqueReferences(cType: ICType, references=new Set<string>()): Set<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: 77b9c3d
@@ -7,6 +7,7 @@ | |||
|
|||
import { hexToU8a } from '@polkadot/util' | |||
import { base58Encode } from '@polkadot/util-crypto' | |||
import * as Kilt from "@kiltprotocol/sdk-js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the credentials package is imported by the sdk-js package this creates a circular dependency and will not work. We also don't do blanket imports in the sdk implementation. Be specific, only import what you need. CType stuff is implemented in the credentials package, this should not be necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: 77b9c3d
// Helper function to check if CType is nested | ||
function cTypeTypeFinder(cType: ICType): boolean { | ||
function hasRef(obj: any): boolean { | ||
if (typeof obj !== 'object' || obj === null) return false | ||
|
||
if ('$ref' in obj) return true | ||
|
||
return Object.values(obj).some(value => { | ||
if (Array.isArray(value)) { | ||
return value.some(item => hasRef(item)) | ||
} | ||
if (typeof value === 'object') { | ||
return hasRef(value) | ||
} | ||
return false | ||
}) | ||
} | ||
return hasRef(cType.properties) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that you're defining a function to call it recursively, but can't you just do that with the outer function directly? Also, more generally, I think all that you need is the second function; if you're already recursing through the CType, collect all the references, we will need them eventually.
function extractUniqueReferences(cType: ICType): Set<string> { | ||
const references = new Set<string>() | ||
|
||
function processValue(value: any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use any, use unknown
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed at: 77b9c3d
} | ||
|
||
// Helper function to extract unique references from CType | ||
function extractUniqueReferences(cType: ICType): Set<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I can somewhat see the point of having an inner function; alternatively, you could solve it by taking an optional second argument:
function extractUniqueReferences(cType: ICType): Set<string> { | |
function extractUniqueReferences(cType: ICType, references=new Set<string>()): Set<string> { |
@@ -344,7 +406,7 @@ export async function validateSubject( | |||
}: Pick<KiltCredentialV1, 'credentialSubject' | 'type'>, | |||
{ | |||
cTypes = [], | |||
loadCTypes = cachingCTypeLoader, | |||
loadCTypes = newCachingCTypeLoader(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the reason for this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at (i missed it that it already exists): 77b9c3d
// Connect to blockchain | ||
const api = Kilt.ConfigService.get('api') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't be needed, we have the loadCTypes argument to load ctypes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed at: 77b9c3d
// Bizim eklediğimiz doğrulama mantığı | ||
const isNested = cTypeTypeFinder(cType) | ||
|
||
if (!isNested) { | ||
await CType.verifyClaimAgainstNestedSchemas( | ||
cType, | ||
[], | ||
claims | ||
) | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip this step; call extractUniqueReferences directly, pass each ctype id to the ctype loader, then pass the resulting definitions to verifyClaimAgainstNestedSchemas. If there are no ctype references in the ctype, this result in an empty array, just like here.
const references = extractUniqueReferences(cType) | ||
const referencedCTypes: ICType[] = [] | ||
|
||
for (const ref of references) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, use array methods here please. E.g., await Promise.all(Array.from(references).map(ctypeId => loadCTypes(ctypeId)))
const referencedCType = await CType.fetchFromChain(ref as any) | ||
if (referencedCType.cType) { | ||
referencedCTypes.push(referencedCType.cType) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, use the loadCTypes argument. If it can't be found, throw an error saying that the ctype contains a reference to another ctype which cannot be resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: 77b9c3d
|
||
// check that we have access to the right schema | ||
let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId) | ||
if (!cType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to somehow handle both the ctypes passed in as the cTypes
argument, and the loadCtypes
function, if there is any. I suggest we create a combined ctype loader function from both, which first checks if the cType whose id is being looked up is contained in the cTypes array, and if not, forwards the id to the loadCtypes
function, which then tries to look it up.
Hint: checking if we have a CType already in cTypes
can be done quicker if we transform the array into a Map of ctype id to ctype definition once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am working on a new commit for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is still tbd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking way better already, but there's still a few things that need to be tackled
const referencedCTypes = await Promise.all( | ||
Array.from(references).map(async (ref) => { | ||
try { | ||
const referencedCType = await cachingCTypeLoader(ref as any) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use the function passed as the loadCtypes
argument
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: bbfced2
// eslint-disable-next-line no-console | ||
console.error('Validation error:', error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't use console statements in library code, that's why the lint rule exists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: bbfced2
// eslint-disable-next-line no-console | ||
console.error(`Failed to fetch CType for reference ${ref}:`, error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't use console statements in library code, that's why the lint rule exists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: bbfced2
// Check recursively if a value has references | ||
const hasRef = (value: unknown): boolean => { | ||
if (typeof value !== 'object' || value === null) { | ||
return false | ||
} | ||
|
||
if ('$ref' in (value as Record<string, unknown>)) { | ||
return true | ||
} | ||
|
||
if (Array.isArray(value)) { | ||
return value.some((item) => hasRef(item)) | ||
} | ||
|
||
return Object.values(value as Record<string, unknown>).some((v) => hasRef(v)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're not using this function any more, are you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes forget to remove, fixed at: bbfced2
if (Array.isArray(value)) { | ||
value.forEach(processValue) | ||
} else { | ||
Object.values(objValue).forEach(processValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the point of passing down the set of references to extractUniqueReferences was that you can call extractUniqueReferences in recursion instead of an inner function. You don't necessarily need to do it that way, but if you're not using this parameter, you can also just pull it back into the function as a normal variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed at: bbfced2
|
||
// check that we have access to the right schema | ||
let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId) | ||
if (!cType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is still tbd
const references = extractUniqueReferences(cType) | ||
|
||
const referencedCTypes = await Promise.all( | ||
Array.from(references).map(async (ref) => { | ||
if (typeof loadCTypes !== 'function') { | ||
throw new Error( | ||
`The definition for this credential's CType ${ref} has not been passed to the validator and CType loading has been disabled` | ||
) | ||
} | ||
return loadCTypes(ref as any) | ||
}) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry that I'm only realising that now, but we have two more issues here that need to be addressed:
- There is a maximum recursion depth for synchronous code in JS, which means that deeply nested structures could end up triggering a max recursion depth reached error
- The CTypes that we fetch could also contain references, so we need to check their definitions too and fetch any CTypes that they depend on
In light of these issues I think it's a good idea to refactor extractUniqueReferences so that it is async
and also loads the ctype definitions directly. This function could be named loadNestedCTypeDefinitions
and would
- Take a CType definition
cType
and a CTypeLoadercTypeLoader
as parameters - Initialise an empty Set
fetchedCTypeIds
- Initialise an empty Set
fetchedCTypeDefinitions
- Define a subroutine
processCTypeProperties
which accepts ICType["properties"] and:- iterates over all properties in the object
- If the property value is an array, call and await
processCTypeProperties
on each item in it and continue - If the property key is $ref, extract the CType id from it
- If the extracted CType id is not yet in
fetchedCTypeIds
, push the id to it, then callcTypeLoader
passing the id as an argument - await the Promise returned by the cTypeLoader; if it's a CType, call
processCTypeProperties
on its properties; else, throw an error
- calls
processCTypeProperties
on the CTypeproperties
and awaits its completion - returns
fetchedCTypeDefinitions
fixes KILTProtocol/ticket#3632
Added nested CType validation support to validateSubject
Implemented automatic reference detection and blockchain fetching
Added support to handle both simple and complex CType structures
Updated tests for new functionality
Note: Improves credential validation by handling nested CTypes automatically.
How to test:
Please provide a brief step-by-step instruction.
If necessary provide information about dependencies (specific configuration, branches, database dumps, etc.)
Checklist: