Skip to content

Commit

Permalink
feature: use storage iri to label data registries
Browse files Browse the repository at this point in the history
Co-authored-by: Maciej Samoraj <[email protected]>
  • Loading branch information
elf-pavlik and samurex committed Nov 17, 2023
1 parent 1c6783b commit 5fb7968
Show file tree
Hide file tree
Showing 16 changed files with 95 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/api-messages/src/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface DataRegistration extends UniqueId {
}

export interface DataRegistry extends UniqueId {
label: string;
registrations: DataRegistration[];
}

Expand Down
9 changes: 4 additions & 5 deletions packages/authorization-agent/test/authorization-agent-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -147,7 +146,7 @@ describe('recordAccessAuthorization', () => {
({
hasDataAuthorization: existingDataAuthorizations,
dataAuthorizations: [] as unknown as AsyncIterable<ReadableDataAuthorization>
} as ReadableAccessAuthorization)
}) as ReadableAccessAuthorization
);

const accessAuthorization = await agent.recordAccessAuthorization(
Expand Down Expand Up @@ -202,7 +201,7 @@ describe('recordAccessAuthorization', () => {
({
hasDataAuthorization: existingDataAuthorizations,
dataAuthorizations: [matchingDataAuthorization] as unknown as AsyncIterable<ReadableDataAuthorization>
} as ReadableAccessAuthorization)
}) as ReadableAccessAuthorization
);

const accessAuthorization = await agent.recordAccessAuthorization(
Expand Down Expand Up @@ -249,7 +248,7 @@ describe('recordAccessAuthorization', () => {
({
hasDataAuthorization: existingDataAuthorizations,
dataAuthorizations: [matchingDataAuthorization] as unknown as AsyncIterable<ReadableDataAuthorization>
} as ReadableAccessAuthorization)
}) as ReadableAccessAuthorization
);

const authorization = {
Expand Down
6 changes: 5 additions & 1 deletion packages/data-model/src/crud/data-registry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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 '.';

export class CRUDDataRegistry extends CRUDContainer {
factory: AuthorizationAgentFactory;

storageIri: string;

get hasDataRegistration(): string[] {
return this.getObjectsArray('hasDataRegistration').map((obj) => obj.value);
}
Expand Down Expand Up @@ -47,6 +49,8 @@ export class CRUDDataRegistry extends CRUDContainer {

async bootstrap(): Promise<void> {
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<CRUDDataRegistry> {
Expand Down
9 changes: 9 additions & 0 deletions packages/data-model/src/readable/inheritable-data-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('/')}/`;
}
}
8 changes: 8 additions & 0 deletions packages/data-model/src/readable/inherited-data-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('/')}/`;
}
}
11 changes: 11 additions & 0 deletions packages/data-model/src/readable/resource.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { DatasetCore } from '@rdfjs/types';
import { getStorageDescription } from '@janeirodigital/interop-utils';
import { Resource } from '..';

export class ReadableResource extends Resource {
protected async fetchData(): Promise<void> {
const response = await this.fetch(this.iri);
this.dataset = await response.dataset();
}

protected async fetchStorageDescription(): Promise<DatasetCore> {
// @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());
}
}
1 change: 1 addition & 0 deletions packages/data-model/test/crud/container-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
12 changes: 9 additions & 3 deletions packages/data-model/test/crud/resoruce-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
if (!this.data) {
Expand All @@ -27,7 +27,11 @@ class CRUDTestResource extends CRUDResource {
}
}

public static async build(iri: string, factory: ApplicationFactory, data?: {}): Promise<CRUDTestResource> {
public static async build(
iri: string,
factory: ApplicationFactory,
data?: { beep: string }
): Promise<CRUDTestResource> {
const instance = new CRUDTestResource(iri, factory, data);
await instance.bootstrap();
return instance;
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion packages/data-model/test/data-instance-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/data-model/test/immutable/resource-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
19 changes: 12 additions & 7 deletions packages/service/src/services/data-registries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const buildDataRegistry = async (
}
return {
id: registry.iri,
label: registry.storageIri,
registrations
};
};
Expand All @@ -43,6 +44,7 @@ const buildDataRegistryForGrant = async (
}
return {
id: registryIri,
label: dataGrants[0].storageIri,
registrations
};
};
Expand All @@ -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<string, DataGrant[]>);
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<string, DataGrant[]>
);
return Promise.all(
Object.entries(dataGrantIndex).map(([registryIri, dataGrants]) =>
buildDataRegistryForGrant(registryIri, dataGrants, descriptionsLang, saiSession)
Expand Down
23 changes: 21 additions & 2 deletions packages/test-utils/src/fetch-mock.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
// 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 <http://www.w3.org/ns/pim/space#Storage> .`
} as unknown as Response;
}

// strip fragment
const strippedUrl = url.replace(/#.*$/, '');
const text = async function text() {
Expand All @@ -20,7 +32,7 @@ async function common(url: string, options?: RequestInit, state?: { [key: string
return 'text/turtle';
}
if (name === 'Link') {
return '<http://just.en.example/description-resource>; rel="describedby"';
return `<http://just.en.example/description-resource>; rel="describedby", <${STORAGE_DESCRIPTION_IRI}>; rel="http://www.w3.org/ns/solid/terms#storageDescription"`;
}
throw Error(`${name} not supported`);
}
Expand Down Expand Up @@ -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;
4 changes: 3 additions & 1 deletion packages/utils/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export interface RdfResponse extends Response {
}

export type WhatwgFetch = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
export type RdfFetch = (iri: string, options?: RdfRequestInit) => Promise<RdfResponse>;
export type RdfFetch = ((iri: string, options?: RdfRequestInit) => Promise<RdfResponse>) & {
raw: WhatwgFetch;
};

// TODO accept either string | NamedNode
// https://github.com/janeirodigital/sai-js/issues/17
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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#');
1 change: 1 addition & 0 deletions packages/utils/test/discovery-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('discoverAuthorizationAgent', () => {
)
])
} as unknown as RdfResponse);
// @ts-ignore
const iri = await discoverAuthorizationAgent(webId, rdfFetch);
expect(iri).toBe(authorizationAgentIri);
});
Expand Down
4 changes: 2 additions & 2 deletions ui/authorization/src/views/DataRegistryList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ span.label {
:key="registry.id">
<v-expansion-panel-title class="d-flex flex-row">
<v-icon color="primary" icon="mdi-hexagon-outline"></v-icon>
<span class="label flex-grow-1">{{ registry.id }}</span>
<span class="label flex-grow-1">{{ registry.label }}</span>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-expansion-panels>
Expand Down Expand Up @@ -74,4 +74,4 @@ watch(
{ immediate: true }
);

</script>
</script>

0 comments on commit 5fb7968

Please sign in to comment.