diff --git a/packages/api-messages/src/payloads.ts b/packages/api-messages/src/payloads.ts index c008ad92..b21e3f07 100644 --- a/packages/api-messages/src/payloads.ts +++ b/packages/api-messages/src/payloads.ts @@ -33,6 +33,7 @@ export interface DataRegistration extends UniqueId { } export interface DataRegistry extends UniqueId { + label: string; registrations: DataRegistration[]; } diff --git a/packages/authorization-agent/test/authorization-agent-test.ts b/packages/authorization-agent/test/authorization-agent-test.ts index a6d2fe58..07f16149 100644 --- a/packages/authorization-agent/test/authorization-agent-test.ts +++ b/packages/authorization-agent/test/authorization-agent-test.ts @@ -14,8 +14,7 @@ import { ReadableDataRegistration, CRUDDataRegistry } from '@janeirodigital/interop-data-model'; -import { asyncIterableToArray } from '@janeirodigital/interop-utils'; -import { ACL, INTEROP } from '@janeirodigital/interop-utils'; +import { asyncIterableToArray, ACL, INTEROP } from '@janeirodigital/interop-utils'; import { AccessAuthorizationStructure, AuthorizationAgent, ShareDataInstanceStructure } from '../src'; const webId = 'https://alice.example/#id'; @@ -147,7 +146,7 @@ describe('recordAccessAuthorization', () => { ({ hasDataAuthorization: existingDataAuthorizations, dataAuthorizations: [] as unknown as AsyncIterable - } as ReadableAccessAuthorization) + }) as ReadableAccessAuthorization ); const accessAuthorization = await agent.recordAccessAuthorization( @@ -202,7 +201,7 @@ describe('recordAccessAuthorization', () => { ({ hasDataAuthorization: existingDataAuthorizations, dataAuthorizations: [matchingDataAuthorization] as unknown as AsyncIterable - } as ReadableAccessAuthorization) + }) as ReadableAccessAuthorization ); const accessAuthorization = await agent.recordAccessAuthorization( @@ -249,7 +248,7 @@ describe('recordAccessAuthorization', () => { ({ hasDataAuthorization: existingDataAuthorizations, dataAuthorizations: [matchingDataAuthorization] as unknown as AsyncIterable - } as ReadableAccessAuthorization) + }) as ReadableAccessAuthorization ); const authorization = { diff --git a/packages/data-model/src/crud/data-registry.ts b/packages/data-model/src/crud/data-registry.ts index 7a1ee98b..d9a3519f 100644 --- a/packages/data-model/src/crud/data-registry.ts +++ b/packages/data-model/src/crud/data-registry.ts @@ -1,5 +1,5 @@ import { DataFactory } from 'n3'; -import { INTEROP } from '@janeirodigital/interop-utils'; +import { INTEROP, RDF, SPACE, getOneMatchingQuad } from '@janeirodigital/interop-utils'; import { ReadableDataRegistration } from '../readable'; import { AuthorizationAgentFactory } from '..'; import { CRUDContainer, CRUDDataRegistration } from '.'; @@ -7,6 +7,8 @@ import { CRUDContainer, CRUDDataRegistration } from '.'; export class CRUDDataRegistry extends CRUDContainer { factory: AuthorizationAgentFactory; + storageIri: string; + get hasDataRegistration(): string[] { return this.getObjectsArray('hasDataRegistration').map((obj) => obj.value); } @@ -47,6 +49,8 @@ export class CRUDDataRegistry extends CRUDContainer { async bootstrap(): Promise { await this.fetchData(); + const storageDescription = await this.fetchStorageDescription(); + this.storageIri = getOneMatchingQuad(storageDescription, null, RDF.type, SPACE.Storage).subject.value; } static async build(iri: string, factory: AuthorizationAgentFactory): Promise { diff --git a/packages/data-model/src/readable/inheritable-data-grant.ts b/packages/data-model/src/readable/inheritable-data-grant.ts index d521f833..4387a396 100644 --- a/packages/data-model/src/readable/inheritable-data-grant.ts +++ b/packages/data-model/src/readable/inheritable-data-grant.ts @@ -27,8 +27,17 @@ export abstract class InheritableDataGrant extends AbstractDataGrant { } // TODO: extract to a mixin + // TODO: change not to rely on / get dataRegistryIri(): string { const dataRegistrationIri = this.getObject('hasDataRegistration').value; return `${dataRegistrationIri.split('/').slice(0, -2).join('/')}/`; } + + // TODO: extract to a mixin + // TODO: change not to rely on / + // TODO: get from storage description like in CRUDDataRegistry + get storageIri(): string { + const dataRegistrationIri = this.getObject('hasDataRegistration').value; + return `${dataRegistrationIri.split('/').slice(0, -3).join('/')}/`; + } } diff --git a/packages/data-model/src/readable/inherited-data-grant.ts b/packages/data-model/src/readable/inherited-data-grant.ts index 72e098de..18f2a7f5 100644 --- a/packages/data-model/src/readable/inherited-data-grant.ts +++ b/packages/data-model/src/readable/inherited-data-grant.ts @@ -43,4 +43,12 @@ export class InheritedDataGrant extends AbstractDataGrant { const dataRegistrationIri = this.getObject('hasDataRegistration').value; return `${dataRegistrationIri.split('/').slice(0, -2).join('/')}/`; } + + // TODO: extract to a mixin + // TODO: change not to rely on / + // TODO: get from storage description like in CRUDDataRegistry + get storageIri(): string { + const dataRegistrationIri = this.getObject('hasDataRegistration').value; + return `${dataRegistrationIri.split('/').slice(0, -3).join('/')}/`; + } } diff --git a/packages/data-model/src/readable/resource.ts b/packages/data-model/src/readable/resource.ts index 4b88a5c5..634b981d 100644 --- a/packages/data-model/src/readable/resource.ts +++ b/packages/data-model/src/readable/resource.ts @@ -1,3 +1,5 @@ +import { DatasetCore } from '@rdfjs/types'; +import { getStorageDescription } from '@janeirodigital/interop-utils'; import { Resource } from '..'; export class ReadableResource extends Resource { @@ -5,4 +7,13 @@ export class ReadableResource extends Resource { const response = await this.fetch(this.iri); this.dataset = await response.dataset(); } + + protected async fetchStorageDescription(): Promise { + // @ts-ignore + const response = await this.fetch.raw(this.iri, { + method: 'HEAD' + }); + const storageDescriptionIri = getStorageDescription(response.headers.get('Link')); + return this.fetch(storageDescriptionIri).then((res) => res.dataset()); + } } diff --git a/packages/data-model/test/crud/container-test.ts b/packages/data-model/test/crud/container-test.ts index 703f3b20..fb3d85ef 100644 --- a/packages/data-model/test/crud/container-test.ts +++ b/packages/data-model/test/crud/container-test.ts @@ -13,6 +13,7 @@ import { AuthorizationAgentFactory, CRUDContainer } from '../../src'; const webId = 'https://alice.example/#id'; const agentId = 'https://jarvis.alice.example/#agent'; const mockedFetch = jest.fn(fetch); +// @ts-ignore const factory = new AuthorizationAgentFactory(webId, agentId, { fetch: mockedFetch, randomUUID }); beforeEach(() => { diff --git a/packages/data-model/test/crud/resoruce-test.ts b/packages/data-model/test/crud/resoruce-test.ts index 37374043..c74ffa0a 100644 --- a/packages/data-model/test/crud/resoruce-test.ts +++ b/packages/data-model/test/crud/resoruce-test.ts @@ -14,10 +14,10 @@ const agentId = 'https://jarvis.alice.example/#agent'; const factory = new AuthorizationAgentFactory(webId, agentId, { fetch, randomUUID }); const snippetIri = 'https://auth.alice.example/bcf22534-0187-4ae4-b88f-fe0f9fa96659'; const newSnippetIri = 'https://auth.alice.example/afb6a337-40df-4fbe-9b00-5c9c1e56c812'; -const data = {}; +const data = { beep: 'boop' }; class CRUDTestResource extends CRUDResource { - data: {}; + data: { beep: string }; protected async bootstrap(): Promise { if (!this.data) { @@ -27,7 +27,11 @@ class CRUDTestResource extends CRUDResource { } } - public static async build(iri: string, factory: ApplicationFactory, data?: {}): Promise { + public static async build( + iri: string, + factory: ApplicationFactory, + data?: { beep: string } + ): Promise { const instance = new CRUDTestResource(iri, factory, data); await instance.bootstrap(); return instance; @@ -36,6 +40,7 @@ class CRUDTestResource extends CRUDResource { describe('update', () => { test('should properly use fetch', async () => { + // @ts-ignore const localFactory = new ApplicationFactory({ fetch: jest.fn(fetch), randomUUID }); const testResource = await CRUDTestResource.build(snippetIri, localFactory); await testResource.update(); @@ -116,6 +121,7 @@ describe('setters', () => { describe('delete', () => { test('should properly use fetch', async () => { + // @ts-ignore const localFactory = new ApplicationFactory({ fetch: jest.fn(fetch), randomUUID }); const testResource = await CRUDTestResource.build(snippetIri, localFactory); await testResource.delete(); diff --git a/packages/data-model/test/data-instance-test.ts b/packages/data-model/test/data-instance-test.ts index 95a9b51d..0d0b566c 100644 --- a/packages/data-model/test/data-instance-test.ts +++ b/packages/data-model/test/data-instance-test.ts @@ -5,9 +5,9 @@ import { fetch } from '@janeirodigital/interop-test-utils'; import { randomUUID } from 'crypto'; import { DatasetCore } from '@rdfjs/types'; import { DataFactory } from 'n3'; -import { DataGrant, DataInstance, ApplicationFactory } from '../src'; import { RDFS } from '@janeirodigital/interop-utils'; import { describe } from 'node:test'; +import { DataGrant, DataInstance, ApplicationFactory } from '../src'; const factory = new ApplicationFactory({ fetch, randomUUID }); const snippetIri = 'https://pro.alice.example/7a130c38-668a-4775-821a-08b38f2306fb#project'; @@ -85,6 +85,7 @@ test('should forward accessMode from the grant', async () => { describe('delete', () => { test('should properly use fetch', async () => { + // @ts-ignore const localFactory = new ApplicationFactory({ fetch: jest.fn(fetch), randomUUID }); const dataInstance = await localFactory.dataInstance(snippetIri, defaultDataGrant); await dataInstance.delete(); @@ -103,6 +104,7 @@ describe('delete', () => { test('should remove reference from parent if a child', async () => { const dataInstance = await DataInstance.build(snippetIri, defaultDataGrant, factory); let taskToDelete; + // eslint-disable-next-line for await (const task of dataInstance.getChildInstancesIterator(taskShapeTree)) { taskToDelete = task; break; @@ -130,6 +132,7 @@ describe('update', () => { }); test('should properly use fetch', async () => { + // @ts-ignore const localFactory = new ApplicationFactory({ fetch: jest.fn(fetch), randomUUID }); const dataInstance = await localFactory.dataInstance(snippetIri, defaultDataGrant); const dataRegistrationIri = 'https://pro.alice.example/773605f0-b5bf-4d46-878d-5c167eac8b5d'; @@ -142,6 +145,7 @@ describe('update', () => { }); test('should set updated dataset on the data instance', async () => { + // @ts-ignore const localFactory = new ApplicationFactory({ fetch: jest.fn(fetch), randomUUID }); const dataInstance = await localFactory.dataInstance(snippetIri, defaultDataGrant); await dataInstance.update(differentDataset); @@ -189,6 +193,7 @@ test('updateAddingChildReference', async () => { test('updateRemovingChildReference', async () => { const dataInstance = await DataInstance.build(snippetIri, defaultDataGrant, factory); let taskToDelete; + // eslint-disable-next-line for await (const task of dataInstance.getChildInstancesIterator(taskShapeTree)) { taskToDelete = task; break; diff --git a/packages/data-model/test/immutable/resource-test.ts b/packages/data-model/test/immutable/resource-test.ts index 849bb286..177bb946 100644 --- a/packages/data-model/test/immutable/resource-test.ts +++ b/packages/data-model/test/immutable/resource-test.ts @@ -12,6 +12,7 @@ const agentId = 'https://alice.jarvis.example/#agent'; describe('put', () => { test('should PUT its data', async () => { + // @ts-ignore const localFactory = new AuthorizationAgentFactory(webId, agentId, { fetch: jest.fn(fetch), randomUUID }); const resource = new ImmutableResource(snippetIri, localFactory, {}); await resource.put(); diff --git a/packages/service/src/services/data-registries.ts b/packages/service/src/services/data-registries.ts index 26175629..a4633ee8 100644 --- a/packages/service/src/services/data-registries.ts +++ b/packages/service/src/services/data-registries.ts @@ -20,6 +20,7 @@ const buildDataRegistry = async ( } return { id: registry.iri, + label: registry.storageIri, registrations }; }; @@ -43,6 +44,7 @@ const buildDataRegistryForGrant = async ( } return { id: registryIri, + label: dataGrants[0].storageIri, registrations }; }; @@ -62,13 +64,16 @@ export const getDataRegistries = async (agentId: string, descriptionsLang: strin if (!socialAgentRegistration.accessGrant) { throw new Error(`missing access grant for social agent: ${agentId}`); } - const dataGrantIndex = socialAgentRegistration.accessGrant.hasDataGrant.reduce((acc, dataGrant) => { - if (!acc[dataGrant.dataRegistryIri]) { - acc[dataGrant.dataRegistryIri] = [] as DataGrant[]; - } - acc[dataGrant.dataRegistryIri].push(dataGrant); - return acc; - }, {} as Record); + const dataGrantIndex = socialAgentRegistration.accessGrant.hasDataGrant.reduce( + (acc, dataGrant) => { + if (!acc[dataGrant.dataRegistryIri]) { + acc[dataGrant.dataRegistryIri] = [] as DataGrant[]; + } + acc[dataGrant.dataRegistryIri].push(dataGrant); + return acc; + }, + {} as Record + ); return Promise.all( Object.entries(dataGrantIndex).map(([registryIri, dataGrants]) => buildDataRegistryForGrant(registryIri, dataGrants, descriptionsLang, saiSession) diff --git a/packages/test-utils/src/fetch-mock.ts b/packages/test-utils/src/fetch-mock.ts index f49cf398..6d484f11 100644 --- a/packages/test-utils/src/fetch-mock.ts +++ b/packages/test-utils/src/fetch-mock.ts @@ -1,10 +1,22 @@ -import { WhatwgFetch, RdfFetch, fetchWrapper } from '@janeirodigital/interop-utils'; import { readFileSync } from 'fs'; +import { WhatwgFetch, RdfFetch, fetchWrapper } from '@janeirodigital/interop-utils'; +const STORAGE_DESCRIPTION_IRI = 'https://fake.example/storage-desription'; const dataFile = new URL('data.json', import.meta.url); const data = JSON.parse(readFileSync(dataFile, 'utf-8')); async function common(url: string, options?: RequestInit, state?: { [key: string]: string }): Promise { + // handle storage description requests + if (url === STORAGE_DESCRIPTION_IRI) { + return { + clone: () => ({}) as unknown as Response, + headers: { + get: () => 'text/turtle' + }, + text: async () => `<${STORAGE_DESCRIPTION_IRI}> a .` + } as unknown as Response; + } + // strip fragment const strippedUrl = url.replace(/#.*$/, ''); const text = async function text() { @@ -20,7 +32,7 @@ async function common(url: string, options?: RequestInit, state?: { [key: string return 'text/turtle'; } if (name === 'Link') { - return '; rel="describedby"'; + return `; rel="describedby", <${STORAGE_DESCRIPTION_IRI}>; rel="http://www.w3.org/ns/solid/terms#storageDescription"`; } throw Error(`${name} not supported`); } @@ -79,3 +91,10 @@ export const statelessFetch = async function statelessFetch(url: string, options } as WhatwgFetch; export const fetch = fetchWrapper(statelessFetch); + +fetch.raw = async () => + ({ + headers: { + get: () => `<${STORAGE_DESCRIPTION_IRI}>; rel="http://www.w3.org/ns/solid/terms#storageDescription"` + } + }) as unknown as Response; diff --git a/packages/utils/src/fetch.ts b/packages/utils/src/fetch.ts index 3725c133..f7816538 100644 --- a/packages/utils/src/fetch.ts +++ b/packages/utils/src/fetch.ts @@ -11,7 +11,9 @@ export interface RdfResponse extends Response { } export type WhatwgFetch = (input: RequestInfo, init?: RequestInit) => Promise; -export type RdfFetch = (iri: string, options?: RdfRequestInit) => Promise; +export type RdfFetch = ((iri: string, options?: RdfRequestInit) => Promise) & { + raw: WhatwgFetch; +}; // TODO accept either string | NamedNode // https://github.com/janeirodigital/sai-js/issues/17 diff --git a/packages/utils/src/namespaces.ts b/packages/utils/src/namespaces.ts index b24ceffc..df5bbc81 100644 --- a/packages/utils/src/namespaces.ts +++ b/packages/utils/src/namespaces.ts @@ -23,3 +23,4 @@ export const ACP = buildNamespace('http://www.w3.org/ns/solid/acp#'); export const SOLID = buildNamespace('http://www.w3.org/ns/solid/terms#'); export const OIDC = buildNamespace('http://www.w3.org/ns/solid/oidc#'); export const DC = buildNamespace('http://purl.org/dc/terms/'); +export const SPACE = buildNamespace('http://www.w3.org/ns/pim/space#'); diff --git a/packages/utils/test/discovery-test.ts b/packages/utils/test/discovery-test.ts index 5d2376e8..009f4fbd 100644 --- a/packages/utils/test/discovery-test.ts +++ b/packages/utils/test/discovery-test.ts @@ -27,6 +27,7 @@ describe('discoverAuthorizationAgent', () => { ) ]) } as unknown as RdfResponse); + // @ts-ignore const iri = await discoverAuthorizationAgent(webId, rdfFetch); expect(iri).toBe(authorizationAgentIri); }); diff --git a/ui/authorization/src/views/DataRegistryList.vue b/ui/authorization/src/views/DataRegistryList.vue index d11252a7..050b03be 100644 --- a/ui/authorization/src/views/DataRegistryList.vue +++ b/ui/authorization/src/views/DataRegistryList.vue @@ -23,7 +23,7 @@ span.label { :key="registry.id"> - {{ registry.id }} + {{ registry.label }} @@ -74,4 +74,4 @@ watch( { immediate: true } ); - \ No newline at end of file +