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 Entities
    1. Get Data Shapes
    2. Get Relationships
    3. Get Reverse Relationships

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()

Or even local-first solutions for keeping all data on-device:

const app = localApp()
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?

Assme something like, SPARQL endpoint or Local storage; we can persist multiple graphs seperate from each other as named graphs. Do we want to do that by default? Do we want to segregate them by db id or app id?

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": boolean,
    "@value": true
  }
})

Values can also be typed as date, time, datTime, uri, integer, float, decimal, or 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 with db.get

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

Get Data Shapes

Get a single item with specified keys:

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

Get Relationships

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"
  }]
}

Get Reverse Relationships

Relationships can be reversed:

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