Skip to content

Commit b73927a

Browse files
committed
feat: Register ldp:relations during UMA registration
1 parent 99bb5f2 commit b73927a

File tree

4 files changed

+94
-23
lines changed

4 files changed

+94
-23
lines changed

packages/css/config/uma/parts/client.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"fetcher": {
1616
"@id": "urn:solid-server:default:UmaFetcher"
1717
},
18+
"identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
1819
"resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }
1920
}
2021
]

packages/css/src/uma/ResourceRegistrar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class ResourceRegistrar extends StaticHandler {
1818

1919
store.on(AS.Create, async (resource: ResourceIdentifier): Promise<void> => {
2020
for (const owner of await this.findOwners(resource)) {
21-
this.umaClient.createResource(resource, await this.findIssuer(owner)).catch((err: Error) => {
21+
this.umaClient.registerResource(resource, await this.findIssuer(owner)).catch((err: Error) => {
2222
this.logger.error(`Unable to register resource ${resource.path}: ${createErrorMessage(err)}`);
2323
});
2424
}

packages/css/src/uma/UmaClient.ts

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {
22
AccessMap,
33
getLoggerFor,
4-
InternalServerError, joinUrl,
4+
IdentifierStrategy,
5+
InternalServerError,
6+
isContainerIdentifier,
7+
joinUrl,
58
KeyValueStorage,
69
NotFoundHttpError,
710
ResourceIdentifier,
@@ -67,7 +70,10 @@ const algMap = {
6770
}
6871

6972
/**
70-
* Client interface for the UMA AS
73+
* Client interface for the UMA AS.
74+
*
75+
* This class uses an EventEmitter and an in-memory map to keep track of registration progress,
76+
* so does not work with worker threads.
7177
*/
7278
export class UmaClient implements SingleThreaded {
7379
protected readonly logger = getLoggerFor(this);
@@ -80,12 +86,14 @@ export class UmaClient implements SingleThreaded {
8086
/**
8187
* @param umaIdStore - Key/value store containing the resource path -> UMA ID bindings.
8288
* @param fetcher - Used to perform requests targeting the AS.
89+
* @param identifierStrategy - Utility functions based on the path configuration of the server.
8390
* @param resourceSet - Will be used to verify existence of resources.
8491
* @param options - JWT verification options.
8592
*/
8693
constructor(
8794
protected readonly umaIdStore: KeyValueStorage<string, string>,
8895
protected readonly fetcher: Fetcher,
96+
protected readonly identifierStrategy: IdentifierStrategy,
8997
protected readonly resourceSet: ResourceSet,
9098
protected readonly options: UmaVerificationOptions = {},
9199
) {
@@ -128,7 +136,7 @@ export class UmaClient implements SingleThreaded {
128136
// This can be a consequence of adding resources in the wrong way (e.g., copying files),
129137
// or other special resources, such as derived resources.
130138
if (await this.resourceSet.hasResource(target)) {
131-
await this.createResource(target, issuer);
139+
await this.registerResource(target, issuer);
132140
umaId = await this.umaIdStore.get(target.path);
133141
} else {
134142
throw new NotFoundHttpError();
@@ -280,9 +288,31 @@ export class UmaClient implements SingleThreaded {
280288
return configuration;
281289
}
282290

283-
public async createResource(resource: ResourceIdentifier, issuer: string): Promise<void> {
291+
/**
292+
* Updates the UMA registration for the given resource on the given issuer.
293+
* This either registers a new UMA identifier or updates an existing one,
294+
* depending on if it already exists.
295+
* For containers, the resource_defaults will be registered,
296+
* for all resources, the resource_relations with the parent container will be registered.
297+
* For the latter, it is possible that the parent container is not registered yet,
298+
* for example, in the case of seeding multiple resources simultaneously.
299+
* In that case the registration will be done immediately,
300+
* and updated with the relations once the parent registration is finished.
301+
*/
302+
public async registerResource(resource: ResourceIdentifier, issuer: string): Promise<void> {
303+
if (this.inProgressResources.has(resource.path)) {
304+
// It is possible a resource is still being registered when an updated registration is already requested.
305+
// To prevent duplicate registrations of the same resource,
306+
// the next call will only happen when the first one is finished.
307+
await once(this.registerEmitter, resource.path);
308+
return this.registerResource(resource, issuer);
309+
}
284310
this.inProgressResources.add(resource.path);
285-
const { resource_registration_endpoint: endpoint } = await this.fetchUmaConfig(issuer);
311+
let { resource_registration_endpoint: endpoint } = await this.fetchUmaConfig(issuer);
312+
const knownUmaId = await this.umaIdStore.get(resource.path);
313+
if (knownUmaId) {
314+
endpoint = joinUrl(endpoint, knownUmaId);
315+
}
286316

287317
const description: ResourceDescription = {
288318
name: resource.path,
@@ -292,38 +322,77 @@ export class UmaClient implements SingleThreaded {
292322
'urn:example:css:modes:create',
293323
'urn:example:css:modes:delete',
294324
'urn:example:css:modes:write',
295-
]
325+
],
296326
};
297327

298-
this.logger.info(`Creating resource registration for <${resource.path}> at <${endpoint}>`);
328+
if (isContainerIdentifier(resource)) {
329+
description.resource_defaults = { 'http://www.w3.org/ns/ldp#contains': description.resource_scopes };
330+
}
299331

300-
const request = {
301-
url: endpoint,
302-
method: 'POST',
332+
// This function can potentially cause multiple asynchronous calls to be required.
333+
// These will be stored in this array so they can be executed simultaneously.
334+
const promises: Promise<void>[] = [];
335+
if (!this.identifierStrategy.isRootContainer(resource)) {
336+
const parentIdentifier = this.identifierStrategy.getParentContainer(resource);
337+
const parentId = await this.umaIdStore.get(parentIdentifier.path);
338+
if (parentId) {
339+
description.resource_relations = { '@reverse': { 'http://www.w3.org/ns/ldp#contains': [ parentId ] } };
340+
} else {
341+
this.logger.warn(`Unable to register parent relationship of ${
342+
resource.path} due to missing parent ID. Waiting for parent registration.`);
343+
344+
promises.push(
345+
once(this.registerEmitter, parentIdentifier.path)
346+
.then(() => this.registerResource(resource, issuer)),
347+
);
348+
// It is possible the parent is not yet being registered.
349+
// We need to force a registration in such a case, otherwise the above event will never be fired.
350+
if (!this.inProgressResources.has(parentIdentifier.path)) {
351+
promises.push(this.registerResource(parentIdentifier, issuer));
352+
}
353+
}
354+
}
355+
356+
this.logger.info(
357+
`${knownUmaId ? 'Updating' : 'Creating'} resource registration for <${resource.path}> at <${endpoint}>`,
358+
);
359+
360+
const request: RequestInit = {
361+
method: knownUmaId ? 'PUT' : 'POST',
303362
headers: {
304363
'Content-Type': 'application/json',
305364
'Accept': 'application/json',
306365
},
307366
body: JSON.stringify(description),
308367
};
309368

310-
return this.fetcher.fetch(endpoint, request).then(async resp => {
311-
if (resp.status !== 201) {
312-
throw new Error (`Resource registration request failed. ${await resp.text()}`);
313-
}
369+
const fetchPromise = this.fetcher.fetch(endpoint, request).then(async resp => {
370+
if (knownUmaId) {
371+
if (resp.status !== 200) {
372+
throw new InternalServerError(`Resource update request failed. ${await resp.text()}`);
373+
}
374+
} else {
375+
if (resp.status !== 201) {
376+
throw new InternalServerError(`Resource registration request failed. ${await resp.text()}`);
377+
}
314378

315-
const { _id: umaId } = await resp.json();
379+
const { _id: umaId } = await resp.json();
316380

317-
if (!umaId || typeof umaId !== 'string') {
318-
throw new Error ('Unexpected response from UMA server; no UMA id received.');
319-
}
381+
if (!isString(umaId)) {
382+
throw new InternalServerError('Unexpected response from UMA server; no UMA id received.');
383+
}
320384

321-
await this.umaIdStore.set(resource.path, umaId);
322-
this.logger.info(`Registered resource ${resource.path} with UMA ID ${umaId}`);
385+
await this.umaIdStore.set(resource.path, umaId);
386+
this.logger.info(`Registered resource ${resource.path} with UMA ID ${umaId}`);
387+
}
323388
// Indicate this resource finished registration
324389
this.inProgressResources.delete(resource.path);
325390
this.registerEmitter.emit(resource.path);
326391
});
392+
393+
// Execute all the required promises.
394+
promises.push(fetchPromise);
395+
await Promise.all(promises);
327396
}
328397

329398
/**

packages/uma/src/views/ResourceDescription.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Type, array, optional as $, string } from "../util/ReType";
2-
import { ScopeDescription } from "./ScopeDescription";
1+
import { Type, array, optional as $, string, dict, union } from '../util/ReType';
32

43
export const ResourceDescription = {
54
resource_scopes: array(string),
5+
resource_defaults: $(union({ '@reverse': dict(array(string)) }, dict(array(string)))),
6+
resource_relations: $(union({ '@reverse': dict(array(string)) }, dict(array(string)))),
67
type: $(string),
78
name: $(string),
89
icon_uri: $(string),

0 commit comments

Comments
 (0)