Skip to content

Commit

Permalink
Improve container updates
Browse files Browse the repository at this point in the history
  • Loading branch information
NoelDeMartin committed Jan 29, 2025
1 parent 9192911 commit bec73d7
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 16 deletions.
111 changes: 111 additions & 0 deletions src/solid/SolidClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,91 @@ describe('SolidClient', () => {
`);
});

it('reuses meta document on subsequent container updates', async () => {
// Arrange
const containerUrl = urlResolveDirectory(faker.internet.url(), stringToSlug(faker.random.word()));
const metaDocumentName = '.' + stringToSlug(faker.random.word());
const descriptionDocumentUrl = containerUrl + metaDocumentName;

StubFetcher.addFetchResponse(`
<${containerUrl}>
a <http://www.w3.org/ns/ldp#Container> ;
<http://www.w3.org/2000/01/rdf-schema#label> "Things" .
`, { Link: `<${metaDocumentName}>; rel="describedBy"` });
StubFetcher.addFetchResponse();
StubFetcher.addFetchResponse(`
<${containerUrl}>
a <http://www.w3.org/ns/ldp#Container> ;
<http://www.w3.org/2000/01/rdf-schema#label> "Updated Things" .
`);
StubFetcher.addFetchResponse();

// Act
await client.updateDocument(containerUrl, [
new UpdatePropertyOperation(
RDFResourceProperty.literal(
containerUrl,
'http://www.w3.org/2000/01/rdf-schema#label',
'Updated Things',
),
),
]);

await client.updateDocument(containerUrl, [
new UpdatePropertyOperation(
RDFResourceProperty.literal(
containerUrl,
'http://www.w3.org/2000/01/rdf-schema#label',
'Updated Things again',
),
),
]);

// Assert
expect(StubFetcher.fetch).toHaveBeenNthCalledWith(1, containerUrl, { headers: { Accept: 'text/turtle' } });
expect(StubFetcher.fetch).toHaveBeenNthCalledWith(
2,
descriptionDocumentUrl,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/sparql-update' },
body: expect.anything(),
},
);
expect(StubFetcher.fetch).toHaveBeenNthCalledWith(
3,
descriptionDocumentUrl,
{ headers: { Accept: 'text/turtle' } },
);
expect(StubFetcher.fetch).toHaveBeenNthCalledWith(
4,
descriptionDocumentUrl,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/sparql-update' },
body: expect.anything(),
},
);

expect(StubFetcher.fetchSpy.mock.calls[1]?.[1]?.body).toEqualSparql(`
DELETE DATA {
<${containerUrl}> <http://www.w3.org/2000/01/rdf-schema#label> "Things" .
} ;
INSERT DATA {
<${containerUrl}> <http://www.w3.org/2000/01/rdf-schema#label> "Updated Things" .
}
`);

expect(StubFetcher.fetchSpy.mock.calls[3]?.[1]?.body).toEqualSparql(`
DELETE DATA {
<${containerUrl}> <http://www.w3.org/2000/01/rdf-schema#label> "Updated Things" .
} ;
INSERT DATA {
<${containerUrl}> <http://www.w3.org/2000/01/rdf-schema#label> "Updated Things again" .
}
`);
});

it('changes resource urls', async () => {
// Arrange
const legacyParentUrl = urlResolveDirectory(faker.internet.url(), stringToSlug(faker.random.word()));
Expand Down Expand Up @@ -744,6 +829,32 @@ describe('SolidClient', () => {
expect(StubFetcher.fetch).not.toHaveBeenCalled();
});

it('ignores idempotent operations', async () => {
// Arrange
const documentUrl = faker.internet.url();
const data = '<> <http://www.w3.org/2000/01/rdf-schema#label> "Things" .';

StubFetcher.addFetchResponse(data);
StubFetcher.addFetchResponse();

// Act
await client.updateDocument(documentUrl, [
new UpdatePropertyOperation(RDFResourceProperty.literal(documentUrl, IRI('rdfs:label'), 'Things')),
new UpdatePropertyOperation(RDFResourceProperty.literal(documentUrl, IRI('foaf:name'), 'Things')),
]);

// Assert
expect(StubFetcher.fetch).toHaveBeenNthCalledWith(2, documentUrl, {
method: 'PATCH',
headers: { 'Content-Type': 'application/sparql-update' },
body: expect.anything(),
});

expect(StubFetcher.fetchSpy.mock.calls[1]?.[1]?.body).toEqualSparql(`
INSERT DATA { <> <http://xmlns.com/foaf/0.1/name> "Things" . }
`);
});

it('updates array properties', async () => {
// Arrange
const personUrl = fakeResourceUrl();
Expand Down
60 changes: 44 additions & 16 deletions src/solid/SolidClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const RESERVED_CONTAINER_TYPES = [
IRI('ldp:BasicContainer'),
];

const containerDescriptionUrls: Map<string, string> = new Map();

// TODO extract file to @noeldemartin/solid-utils

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -158,6 +160,8 @@ export default class SolidClient {
return;
}

url = containerDescriptionUrls.get(url) ?? url;

const document = await this.getDocument(url);

if (document === null)
Expand Down Expand Up @@ -393,8 +397,12 @@ export default class SolidClient {
}

private async updateContainerDocument(document: RDFDocument, operations: UpdateOperation[]): Promise<void> {
const descriptionUrl = document.metadata.describedBy || `${document.url}.meta`;

document.url && containerDescriptionUrls.set(document.url, descriptionUrl);

await this.updateNonContainerDocument(
document.clone({ changeUrl: document.metadata.describedBy || `${document.url}.meta` }),
document.clone({ changeUrl: descriptionUrl }),
operations,
);
}
Expand All @@ -421,24 +429,23 @@ export default class SolidClient {
operations: UpdateOperation[],
): Promise<void> {
const [updatedProperties, removedProperties] = decantUpdateOperationsData(operations);
const inserts = RDFResourceProperty.toTurtle(updatedProperties.flat(), document.url);
const deletes = RDFResourceProperty.toTurtle(
document.properties.filter(
property =>
removedProperties.some(([resourceUrl, name, value]) =>
resourceUrl === property.resourceUrl &&
(!name || name === property.name) &&
(!value || value === property.value)),
const inserts = updatedProperties.flat().map(property => property.toTurtle(document.url) + ' .');
const deletes = document.properties.filter(
property => removedProperties.some(
([resourceUrl, name, value]) =>
resourceUrl === property.resourceUrl &&
(!name || name === property.name) &&
(!value || value === property.value),
),
document.url,
);
)
.map(property => property.toTurtle(document.url) + ' .');

const response = await this.fetch(document.url as string, {
method: 'PATCH',
headers: { 'Content-Type': 'application/sparql-update' },
body: arrayFilter([
deletes.length > 0 && `DELETE DATA { ${deletes} }`,
inserts.length > 0 && `INSERT DATA { ${inserts} }`,
deletes.length > 0 && `DELETE DATA { ${deletes.join('\n')} }`,
inserts.length > 0 && `INSERT DATA { ${inserts.join('\n')} }`,
]).join(' ; '),
});

Expand Down Expand Up @@ -533,20 +540,40 @@ export default class SolidClient {
}

// TODO this method should remove all UpdatePropertyOperation and use only
// SetPropertyOperation and RemovePropertyOperation
// SetPropertyOperation and RemovePropertyOperation for better performance
private processUpdatePropertyOperations(document: RDFDocument, operations: UpdateOperation[]): void {
// Diff arrays
const arrayOperations: UpdateOperation[] = [];
const idempotentOperations: UpdateOperation[] = [];
const arrayProperties: string[] = [];

for (let index = 0; index < operations.length; index++) {
const operation = operations[index] as UpdateOperation;

if (operation.type !== OperationType.UpdateProperty)
if (operation.type !== OperationType.UpdateProperty) {
continue;
}

if (operation.propertyResourceUrl === null) {
continue;
}

if (!Array.isArray(operation.propertyOrProperties)) {
const property = operation.propertyOrProperties;
const [currentProperty, ...otherProperties] =
document.resource(property.resourceUrl ?? '')?.propertiesIndex[property.name] ?? [];

if (
currentProperty &&
otherProperties.length === 0 &&
property.value === currentProperty.value &&
property.type === currentProperty.type
) {
idempotentOperations.push(operation);
}

if (!Array.isArray(operation.propertyOrProperties) || operation.propertyResourceUrl === null)
continue;
}

const documentValues = document
.statements
Expand Down Expand Up @@ -595,6 +622,7 @@ export default class SolidClient {
}

arrayOperations.forEach(operation => arrayRemove(operations, operation));
idempotentOperations.forEach(operation => arrayRemove(operations, operation));

// Properties that are going to be updated have to be deleted or they'll end up duplicated.
const updateOperations = operations.filter(
Expand Down

0 comments on commit bec73d7

Please sign in to comment.