Writing Data
Patterns for creating, updating, and deleting RDF data in Solid pods.
Basic Updates
Add a Statement
import { store, sym, lit, st } from 'solid-logic'
async function addStatement(subject, predicate, object, doc) {
await store.updater.update(
[], // deletions
[st(sym(subject), predicate, object, sym(doc))] // insertions
)
}
// Usage
await addStatement(
'https://alice.example/profile/card#me',
FOAF('name'),
lit('Alice Smith'),
'https://alice.example/profile/card'
)
Remove a Statement
async function removeStatement(subject, predicate, object, doc) {
await store.updater.update(
[st(sym(subject), predicate, object, sym(doc))], // deletions
[] // insertions
)
}
Update a Value
async function updateValue(subject, predicate, newValue, doc) {
const subj = sym(subject)
const document = sym(doc)
// Find current value
const oldValue = store.any(subj, predicate, null, document)
await store.updater.update(
oldValue ? [st(subj, predicate, oldValue, document)] : [],
[st(subj, predicate, newValue, document)]
)
}
// Usage
await updateValue(
'https://alice.example/profile/card#me',
FOAF('name'),
lit('Alice Johnson'),
'https://alice.example/profile/card'
)
Creating Resources
Create a New Document
async function createDocument(uri, statements) {
const doc = sym(uri)
// Add statements to local store first
statements.forEach(statement => store.add(statement))
// PUT to server
await store.updater.put(
doc,
statements,
'text/turtle'
)
}
// Usage
const noteUri = 'https://alice.example/notes/note1.ttl'
const note = sym(`${noteUri}#this`)
await createDocument(noteUri, [
st(note, RDF('type'), sym('http://schema.org/Note'), sym(noteUri)),
st(note, RDFS('label'), lit('My Note'), sym(noteUri)),
st(note, sym('http://schema.org/text'), lit('Note content here'), sym(noteUri)),
])
Create a Container
async function createContainer(parentUri, name) {
const containerUri = `${parentUri}${name}/`
await store.fetcher.createContainer(sym(parentUri), name)
return containerUri
}
// Usage
await createContainer('https://alice.example/', 'documents')
// Creates https://alice.example/documents/
Create a Resource with Slug
async function createWithSlug(containerUri, slug, statements) {
// Use POST with Slug header
const response = await store.fetcher.webOperation('POST', containerUri, {
headers: {
'Content-Type': 'text/turtle',
'Slug': slug
},
body: serialize(null, store, containerUri, 'text/turtle')
})
return response.headers.get('Location')
}
Common Write Patterns
Update Profile
async function updateProfile(webId, updates) {
const person = sym(webId)
const doc = person.doc()
const deletions = []
const insertions = []
if ('name' in updates) {
const oldName = store.any(person, FOAF('name'), null, doc)
if (oldName) deletions.push(st(person, FOAF('name'), oldName, doc))
if (updates.name) insertions.push(st(person, FOAF('name'), lit(updates.name), doc))
}
if ('bio' in updates) {
const oldBio = store.any(person, FOAF('bio'), null, doc)
if (oldBio) deletions.push(st(person, FOAF('bio'), oldBio, doc))
if (updates.bio) insertions.push(st(person, FOAF('bio'), lit(updates.bio), doc))
}
if ('photo' in updates) {
const oldPhoto = store.any(person, FOAF('img'), null, doc)
if (oldPhoto) deletions.push(st(person, FOAF('img'), oldPhoto, doc))
if (updates.photo) insertions.push(st(person, FOAF('img'), sym(updates.photo), doc))
}
await store.updater.update(deletions, insertions)
}
Add to a List
async function addFriend(myWebId, friendWebId) {
const me = sym(myWebId)
const friend = sym(friendWebId)
const doc = me.doc()
await store.updater.update([], [
st(me, FOAF('knows'), friend, doc)
])
}
async function removeFriend(myWebId, friendWebId) {
const me = sym(myWebId)
const friend = sym(friendWebId)
const doc = me.doc()
await store.updater.update([
st(me, FOAF('knows'), friend, doc)
], [])
}
Create a Note
async function createNote(containerUri, title, content) {
const noteDoc = sym(`${containerUri}${Date.now()}.ttl`)
const note = sym(`${noteDoc.uri}#this`)
const now = new Date().toISOString()
const dateTimeLit = lit(now, null, sym('http://www.w3.org/2001/XMLSchema#dateTime'))
const statements = [
st(note, RDF('type'), sym('http://schema.org/Note'), noteDoc),
st(note, RDFS('label'), lit(title), noteDoc),
st(note, sym('http://schema.org/text'), lit(content), noteDoc),
st(note, DCT('created'), dateTimeLit, noteDoc),
st(note, DCT('creator'), authn.currentUser(), noteDoc),
]
await store.updater.put(noteDoc, statements, 'text/turtle')
return note
}
Deleting Resources
Delete a Document
async function deleteDocument(uri) {
await store.fetcher.delete(sym(uri))
}
Delete a Container
async function deleteContainer(uri) {
const container = sym(uri)
await store.fetcher.load(container)
// First, delete all contents
const contents = store.each(container, LDP('contains'), null, null)
for (const item of contents) {
const isContainer = store.holds(item, RDF('type'), LDP('Container'), null)
if (isContainer) {
await deleteContainer(item.uri)
} else {
await store.fetcher.delete(item)
}
}
// Then delete the container
await store.fetcher.delete(container)
}
File Operations
Upload a File
async function uploadFile(containerUri, file) {
const fileUri = `${containerUri}${file.name}`
await store.fetcher.webOperation('PUT', fileUri, {
headers: {
'Content-Type': file.type
},
body: file
})
return fileUri
}
// Usage with file input
const input = document.querySelector('input[type="file"]')
input.addEventListener('change', async (e) => {
const file = e.target.files[0]
const uri = await uploadFile('https://alice.example/photos/', file)
console.log('Uploaded:', uri)
})
Upload with RDF Metadata
async function uploadWithMetadata(containerUri, file, metadata) {
// Upload the file
const fileUri = await uploadFile(containerUri, file)
// Create metadata document
const metaDoc = sym(`${fileUri}.meta`)
const fileNode = sym(fileUri)
const statements = [
st(fileNode, RDFS('label'), lit(metadata.title || file.name), metaDoc),
st(fileNode, DCT('created'), lit(new Date().toISOString(), null,
sym('http://www.w3.org/2001/XMLSchema#dateTime')), metaDoc),
]
if (metadata.description) {
statements.push(st(fileNode, DCT('description'), lit(metadata.description), metaDoc))
}
await store.updater.put(metaDoc, statements, 'text/turtle')
return fileUri
}
Error Handling
Handle Update Errors
async function safeUpdate(deletions, insertions) {
try {
await store.updater.update(deletions, insertions)
return { success: true }
} catch (error) {
if (error.status === 401) {
return { success: false, reason: 'Not authenticated' }
} else if (error.status === 403) {
return { success: false, reason: 'No write permission' }
} else if (error.status === 409) {
return { success: false, reason: 'Conflict - resource changed' }
} else if (error.status === 412) {
return { success: false, reason: 'Precondition failed' }
} else {
return { success: false, reason: error.message }
}
}
}
Retry on Conflict
async function updateWithRetry(subject, predicate, newValue, doc, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
// Reload to get latest state
await store.fetcher.load(sym(doc), { force: true })
// Get current value
const oldValue = store.any(sym(subject), predicate, null, sym(doc))
// Apply update
await store.updater.update(
oldValue ? [st(sym(subject), predicate, oldValue, sym(doc))] : [],
[st(sym(subject), predicate, newValue, sym(doc))]
)
return { success: true }
} catch (error) {
if (error.status === 409 && i < maxRetries - 1) {
// Conflict, retry
continue
}
throw error
}
}
}
Batch Operations
Multiple Updates at Once
async function batchUpdate(operations) {
const allDeletions = []
const allInsertions = []
operations.forEach(op => {
if (op.type === 'delete') {
allDeletions.push(st(sym(op.subject), op.predicate, op.object, sym(op.doc)))
} else if (op.type === 'insert') {
allInsertions.push(st(sym(op.subject), op.predicate, op.object, sym(op.doc)))
}
})
await store.updater.update(allDeletions, allInsertions)
}
See Also
- rdflib.js — UpdateManager
- Reading Data — load and query
- Access Control — permissions