// The items of a pod collection, read and checked with the pod's answer SEEN. // // The platform's `pod-collections` service (@activitypods/app 2.3.2) hides a failure of the pod: // `getItems` answers `[]` whatever the GET returned (it never looks at the status), and `add` / // `remove` resolve normally when the pod refuses the PATCH (a status ≥ 400 is only logged). A // decision taken on such an answer is wrong without anything saying so: a Leave read against a // failed read finds nobody to remove, and an act whose write was refused is recorded as applied. // // So the backend reads a collection through `pod-resources.get`, where the status is visible, and // verifies every write by reading the collection back. The writes themselves stay the platform's // calls (`pod-collections.add` / `.remove`, through the collection handlers' `add` / `remove`). // // readItems(ctx, { collectionUri, actorUri }) → the item URIs; throws unless the pod answers 200 // with that very collection, its items inline (a paged collection is not read here: its items // are not in the root, and reading them as "none" is the defect this module exists to avoid). // expectItem(ctx, { collectionUri, itemUri, actorUri, present }) → resolves when the collection, // read again, holds `itemUri` (`present`) or does not (`!present`); throws otherwise — the write // just made did not take effect. const { arrayOf } = require('@semapps/ldp'); const { uriOf } = require('./emitted-activity'); const AS = 'https://www.w3.org/ns/activitystreams#'; /** The item URIs a collection node lists inline, under any spelling of `items` / `orderedItems`. */ function itemsOf(collection) { const keys = ['items', 'orderedItems', 'as:items', 'as:orderedItems', `${AS}items`, `${AS}orderedItems`]; const value = keys.map(key => collection[key]).find(v => v !== undefined); return arrayOf(value) .map(uriOf) .filter(uri => typeof uri === 'string'); } /** Whether a collection root sends its items to pages (`first`) instead of listing them. */ const isPaged = collection => ['first', 'as:first', `${AS}first`].some(key => collection[key] !== undefined) && itemsOf(collection).length === 0; async function readItems(ctx, { collectionUri, actorUri }) { const { status, body } = await ctx.call('pod-resources.get', { resourceUri: collectionUri, actorUri }); if (status !== 200 || !body || typeof body !== 'object') { throw new Error(`the pod does not return the collection ${collectionUri} (HTTP ${status})`); } if (uriOf(body) !== collectionUri) throw new Error(`the pod returned another resource for ${collectionUri}`); if (isPaged(body)) throw new Error(`the collection ${collectionUri} is paged: its items cannot be read from its root`); return itemsOf(body); } async function expectItem(ctx, { collectionUri, itemUri, actorUri, present }) { const holds = (await readItems(ctx, { collectionUri, actorUri })).includes(itemUri); if (holds !== present) { throw new Error(`${collectionUri} ${present ? 'does not hold' : 'still holds'} ${itemUri} after the ${present ? 'add' : 'remove'}: the pod did not apply it`); } } module.exports = { itemsOf, readItems, expectItem };