import { Algebra } from 'sparqlalgebrajs'; import type { Operation } from '../../http/Operation'; import type { Representation } from '../../http/representation/Representation'; import type { SparqlUpdatePatch } from '../../http/representation/SparqlUpdatePatch'; import type { ResourceSet } from '../../storage/ResourceSet'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { IdentifierSetMultiMap } from '../../util/map/IdentifierMap'; import { ModesExtractor } from './ModesExtractor'; import type { AccessMap } from './Permissions'; import { AccessMode } from './Permissions'; /** * Generates permissions for a SPARQL DELETE/INSERT body. * Updates with only an INSERT can be done with just append permissions, * while DELETEs require write permissions as well. */ export class SparqlUpdateModesExtractor extends ModesExtractor { private readonly resourceSet: ResourceSet; /** * Certain permissions depend on the existence of the target resource. * The provided {@link ResourceSet} will be used for that. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence. */ public constructor(resourceSet: ResourceSet) { super(); this.resourceSet = resourceSet; } public async canHandle({ body }: Operation): Promise { if (!this.isSparql(body)) { throw new NotImplementedHttpError('Cannot determine permissions of non-SPARQL patches.'); } if (!this.isSupported(body.algebra)) { throw new NotImplementedHttpError('Can only determine permissions of a PATCH with DELETE/INSERT operations.'); } } public async handle({ body, target }: Operation): Promise { // Verified in `canHandle` call const update = (body as SparqlUpdatePatch).algebra as Algebra.DeleteInsert; const requiredModes: AccessMap = new IdentifierSetMultiMap(); if (this.isNop(update)) { return requiredModes; } // Access modes inspired by the requirements on N3 Patch requests if (this.hasConditions(update)) { requiredModes.add(target, AccessMode.read); } if (this.hasInserts(update)) { requiredModes.add(target, AccessMode.append); if (!await this.resourceSet.hasResource(target)) { requiredModes.add(target, AccessMode.create); } } if (this.hasDeletes(update)) { requiredModes.add(target, AccessMode.read); requiredModes.add(target, AccessMode.write); } return requiredModes; } private isSparql(data: Representation): data is SparqlUpdatePatch { return Boolean((data as SparqlUpdatePatch).algebra); } private isSupported(op: Algebra.Update): boolean { if (this.isDeleteInsert(op) || this.isNop(op)) { return true; } if (op.type === Algebra.types.COMPOSITE_UPDATE) { return op.updates.every((update): boolean => this.isSupported(update)); } return false; } private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert { return op.type === Algebra.types.DELETE_INSERT; } private isNop(op: Algebra.Operation): op is Algebra.Nop { return op.type === Algebra.types.NOP; } private hasConditions(update: Algebra.Update): boolean { if (this.isDeleteInsert(update)) { return Boolean(update.where && !this.isNop(update.where)); } return (update as Algebra.CompositeUpdate).updates.some((op): boolean => this.hasConditions(op)); } private hasInserts(update: Algebra.Update): boolean { if (this.isDeleteInsert(update)) { return Boolean(update.insert && update.insert.length > 0); } return (update as Algebra.CompositeUpdate).updates.some((op): boolean => this.hasInserts(op)); } private hasDeletes(update: Algebra.Update): boolean { if (this.isDeleteInsert(update)) { return Boolean(update.delete && update.delete.length > 0); } return (update as Algebra.CompositeUpdate).updates.some((op): boolean => this.hasDeletes(op)); } }