@@ -69,6 +69,22 @@ export interface FetchOptions {
6969 timeout ?: number ;
7070}
7171
72+ export interface PatchResult {
73+ success : boolean ;
74+ status : number ;
75+ statusText : string ;
76+ }
77+
78+ /**
79+ * Represents an RDF term that can be serialized into SPARQL or N3.
80+ * Use angle brackets for IRIs, quotes for literals, prefixed names as-is.
81+ */
82+ export interface RdfTerm {
83+ value : string ;
84+ /** 'iri' wraps in <>, 'literal' wraps in "", 'prefixed' emits as-is */
85+ type : 'iri' | 'literal' | 'prefixed' ;
86+ }
87+
7288// --- Service Implementation ---
7389
7490class JssOntologyService {
@@ -640,6 +656,151 @@ class JssOntologyService {
640656 return Array . isArray ( type ) ? type : [ type ] ;
641657 }
642658
659+ // --- SPARQL PATCH Mutations ---
660+
661+ /**
662+ * Send a SPARQL Update PATCH to the ontology resource.
663+ * Uses Content-Type: application/sparql-update as per Solid Protocol.
664+ */
665+ public async patchOntology ( sparqlUpdate : string ) : Promise < PatchResult > {
666+ const url = this . getOntologyUrl ( ) ;
667+
668+ try {
669+ const response = await this . fetchWithAuth ( url , {
670+ method : 'PATCH' ,
671+ headers : {
672+ 'Content-Type' : 'application/sparql-update' ,
673+ } ,
674+ body : sparqlUpdate ,
675+ } ) ;
676+
677+ if ( response . ok ) {
678+ this . invalidateCache ( ) ;
679+ }
680+
681+ if ( debugState . isEnabled ( ) ) {
682+ logger . info ( 'SPARQL PATCH sent' , {
683+ status : response . status ,
684+ bodyLength : sparqlUpdate . length ,
685+ } ) ;
686+ }
687+
688+ return {
689+ success : response . ok ,
690+ status : response . status ,
691+ statusText : response . statusText ,
692+ } ;
693+ } catch ( error ) {
694+ logger . error ( 'SPARQL PATCH failed' , createErrorMetadata ( error ) ) ;
695+ throw error ;
696+ }
697+ }
698+
699+ /**
700+ * Send an N3 Patch to the ontology resource.
701+ * Uses Content-Type: text/n3 for optimistic concurrency via solid:where clauses.
702+ *
703+ * N3 Patch format (Solid Protocol):
704+ * @prefix solid: <http://www.w3.org/ns/solid/terms#>.
705+ * _:patch a solid:InsertDeletePatch;
706+ * solid:where { ?cond ... };
707+ * solid:deletes { ?old ... };
708+ * solid:inserts { ?new ... }.
709+ */
710+ public async patchOntologyN3 ( n3Patch : string ) : Promise < PatchResult > {
711+ const url = this . getOntologyUrl ( ) ;
712+
713+ try {
714+ const response = await this . fetchWithAuth ( url , {
715+ method : 'PATCH' ,
716+ headers : {
717+ 'Content-Type' : 'text/n3' ,
718+ } ,
719+ body : n3Patch ,
720+ } ) ;
721+
722+ if ( response . ok ) {
723+ this . invalidateCache ( ) ;
724+ }
725+
726+ if ( debugState . isEnabled ( ) ) {
727+ logger . info ( 'N3 PATCH sent' , {
728+ status : response . status ,
729+ bodyLength : n3Patch . length ,
730+ } ) ;
731+ }
732+
733+ return {
734+ success : response . ok ,
735+ status : response . status ,
736+ statusText : response . statusText ,
737+ } ;
738+ } catch ( error ) {
739+ logger . error ( 'N3 PATCH failed' , createErrorMetadata ( error ) ) ;
740+ throw error ;
741+ }
742+ }
743+
744+ // --- Triple Mutation Helpers ---
745+
746+ /**
747+ * Add a single triple to the ontology via SPARQL INSERT DATA.
748+ */
749+ public async addOntologyTriple (
750+ subject : RdfTerm ,
751+ predicate : RdfTerm ,
752+ object : RdfTerm
753+ ) : Promise < PatchResult > {
754+ const sparql = `INSERT DATA {\n ${ this . serializeTerm ( subject ) } ${ this . serializeTerm ( predicate ) } ${ this . serializeTerm ( object ) } .\n}` ;
755+ return this . patchOntology ( sparql ) ;
756+ }
757+
758+ /**
759+ * Remove a single triple from the ontology via SPARQL DELETE DATA.
760+ */
761+ public async removeOntologyTriple (
762+ subject : RdfTerm ,
763+ predicate : RdfTerm ,
764+ object : RdfTerm
765+ ) : Promise < PatchResult > {
766+ const sparql = `DELETE DATA {\n ${ this . serializeTerm ( subject ) } ${ this . serializeTerm ( predicate ) } ${ this . serializeTerm ( object ) } .\n}` ;
767+ return this . patchOntology ( sparql ) ;
768+ }
769+
770+ /**
771+ * Update a triple's object value via SPARQL DELETE/INSERT WHERE.
772+ * Atomically removes the old value and inserts the new one.
773+ */
774+ public async updateOntologyTriple (
775+ subject : RdfTerm ,
776+ predicate : RdfTerm ,
777+ oldValue : RdfTerm ,
778+ newValue : RdfTerm
779+ ) : Promise < PatchResult > {
780+ const s = this . serializeTerm ( subject ) ;
781+ const p = this . serializeTerm ( predicate ) ;
782+ const sparql = [
783+ `DELETE { ${ s } ${ p } ${ this . serializeTerm ( oldValue ) } . }` ,
784+ `INSERT { ${ s } ${ p } ${ this . serializeTerm ( newValue ) } . }` ,
785+ `WHERE { ${ s } ${ p } ${ this . serializeTerm ( oldValue ) } . }` ,
786+ ] . join ( '\n' ) ;
787+ return this . patchOntology ( sparql ) ;
788+ }
789+
790+ /**
791+ * Serialize an RdfTerm into its SPARQL string representation.
792+ */
793+ private serializeTerm ( term : RdfTerm ) : string {
794+ switch ( term . type ) {
795+ case 'iri' :
796+ return `<${ term . value } >` ;
797+ case 'literal' :
798+ return `"${ term . value . replace ( / \\ / g, '\\\\' ) . replace ( / " / g, '\\"' ) } "` ;
799+ case 'prefixed' :
800+ return term . value ;
801+ }
802+ }
803+
643804 // --- Public Getters ---
644805
645806 public isConnected ( ) : boolean {
0 commit comments