Solid State KV

An example of how to authenticate an application with Solid, and persist data to a Solid Pod.

Explore how it works on the KV Playground

Table of Contents

  1. Applications
  2. Entities
  3. Persist Data
    1. Post
    2. Put
    3. Patch
    4. Delete
  4. Relationships
  5. Typed Values
  6. Schemas
  7. Getting Entries
  8. Queries
    1. Simple Queries
    2. Compound Queries
    3. Pagination
  9. Introspection
  10. Type Checking

Applications


const appConfig = {
  redirect_uri,
  client_id
}
const app = solidApp(appConfig)
const db = app.webID(url)
<!-- or -->
const db = app.provider(URL)
    

Other applications exist too, for non-SOLID SPARQL providers:


const appConfig = {
  endpoint,
  username,
  password
}
const app = sparqlApp(appConfig)
const db = app()
    
If we store a local cache to speed up requests, how can that cache (which we can set locally without being authenticated) interact with the remote store in a unauthenticated session?

Entities

Entities are stored as flat objects, that is sets of key:value pairs. Values can be of any given type. Nested objects are created by managing relationships between entities.

Entities have two special keys: @id and @type.

The @id key is used to uniquely identify the entity.

The @type key is used to classify or attach a schema to the entity.

Persist Data

An Entity is identified by its @id. Create a new item \ with db.post.


let ref = await db.post("hummus", {
  "@type": "Food",
  "ingredient": "Chickpeas and Lemon"
})
ref = {
  "@id": "hummus",
  "@type": "Food",
  "ingredient": "Chickpeas and Lemon"
}
    

Post

Use db.post to replace an entire entity with a new value:


let ref = await db.post("hummus", {
  "@type": "Dish",
  "ingredient": "Garbanzo Beans"
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "Garbanzo Beans"
}
  

Put

Replace the value of a given key with db.put.


let ref = await db.put('hummus, {
  "ingredient": "Chickpeas",
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "Chickpeas"
}
    

Put and Post are similar but distinct! Post will replace the entire Entity, but Put will replace jut the specified keys.

Patch

Add a value to a key with db.patch


let ref = await db.patch('hummus', {
  ingredient: "Lemon",
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": ["Chickpeas", "Lemon"]
}
    

Delete

Delete a key/value pair with db.delete


let ref = await db.delete(@id, {
  "ingredient": "Lemon",
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "Chickpeas"
}
    

Or delete the entire entity


let ref = await db.delete('hummus')
ref = null
    

Relationships

Entities can be related to each other.


let hummus = await db.post("hummus", {
  "@type": "Dish"
})
let chickpeas = await db.post("chickpeas", {
  "@type": "Ingredient",
  "aka": "Garbanzo Beans"
})

let ref = await db.put('hummus, {
  "ingredient": {
    "@id": "chickpeas"
  }
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "chickpeas"
}
    

Typed Values

Values can be strings, booleans, and numbers. Types can be specified as well:


db.put("chickpeas", {
  "aka": {
    @type: String,
    value: "Garbs"
  }
})
db.put("chickpeas", {
  "gramsPerUnit": {
    @type: Number,
    value: 42
  }
})
db.put("chickpeas", {
  "vegetarian": {
    @type: Number,
    value: true
  }
})
    

Values can also be typed as Date, Time, DateTime, URI, Integer, Float, Decimal, Double.

Schemas

Entities can be given schemas with the @type key. A @type can be defined with a schema:


let dishSchema = await db.schema("Dish", {
  "name": String,
  "ingredient": ID
})

let ingredientSchema = await db.schema("Ingredient", {
  "name": String,
  "aka": String,
  "vegetarian": Boolean
})
    

Keys can also be given data:


let ref = await db.post("vegetarian", {
  "description": "Does this ingredient made entirely from non-animal products?"
})

Okay but _why_ would you do this?

Keys can also be given schemas:


let vegSchema = await db.schema('vegetarian', {
  "description": String,
  "@domain": "Ingredient",
  "@range": Boolean
})
    

See above. Does this enable static type checking by surfacing errors? Can we use this to do introspection?

Getting Entities

Get a single item by @id:


let ref = await db.get('hummus')
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "name": "Hummus",
  "ingredient": ["chickpeas", "lemon"]
}
    

Get a single item with specified keys:


let ref = await db.get('hummus', {
  "ingredient": ""
})
ref = {
  "@id": "hummus",
  "ingredient": ["chickpeas", "lemon"]
}
    

Get related items with specified keys:


let ref = await db.get('hummus', {
  "ingredient": {
    "name": "",
    "aka": ""
  }
})
ref = {
  "@id": "hummus",
  "ingredient": [{
    "name": "Chickpeas",
    "aka": ["Garbanzos", "Garbs"]
  },{
    "name": "Lemon"
  }]
}
    

Relationships can be reversed:


let ref = await db.get('chickpeas', {
  "name": "",
  "usedIn": {
    "@reverse": "ingredient"
  }
})
ref = {
  "@id": chickpeas,
  "usedIn": "hummus"
}
    

Queries

Let's say we want to get entities by values other then their @id. How can we do this?

Querying Simple Values


try {
  let collection = await db.query(
    where(key, operator, value)
  )
} catch (e) {
  console.error("Error querying the store: ", e);
}

try {
  let collection = await db.query(
    or (
      where(key, operator, value),
      where(key, operator, other)
    )
  )
} catch (e) {
  console.error("Error querying the store: ", e);
}