BeagleLab / voyage

Planning for the Beagle Project
4 stars 1 forks source link

New Schema ? #50

Closed jbenet closed 9 years ago

jbenet commented 9 years ago
// Everything is an "entity". it has an ID.
type Entity struct {
  ID EntityID
}

// EntityIDs are just strings. 
// (will be a UUID or something like it)
type EntityID string

// An AccountID is just an EntityID.
type AccountID EntityID

// Account is an object representing a User or a Group.
// (i.e. a person or an organization). this is here
// because permissions need something common
type Account struct {
  ID     AccountID
  Name   string // full name of the account
  Avatar string // a square picture for the account
  Email  string // email associated with account
}

// User is an individual user, a Person.
type User struct {
  Account // it is an account.
  OAuthTokens []string 
}

// Group is a group of people, an organization.
type Group struct {
  Account // it is an account.
  Owner   []AccountID
  Members Membership
}

// Conversation is a semantic grouping of entities, 
// users/groups discussing a topic, through messages
// and reference materials.
type Conversation struct {
  ID Entity

  Title string
  Owner []AccountID
  Participants Membership
}

// A Note is a piece of text related to a conversation.
type Note struct {
  ID Entity

  Text string
  Owner []AccountID
  Participants Membership
}

// Type of permission.
type PermType int

var (
  ReadPerm PermType = iota
  WritePerm
  SharePerm
)

// Membership is a group of users that belong to an entity,
// with associated permissions. For example, the participants
// of a conversation, or the members of a group.
type Membership map[AccountID]Permission

// these are like tables
var (
  EntityPermissions map[AccountID][EntityID]PermType
) 
jbenet commented 9 years ago

needs

RichardLitt commented 9 years ago

Attempted to apply to JSON. Not sure how useful this is: https://github.com/BeagleLab/beagle/commit/8f3a55861b0a2e202b04be9e16a01c6b8e626a4f

jbenet commented 9 years ago

more:

// Everything is an "entity". it has an ID.
type Entity struct {
  ID EntityID
}

// EntityIDs are just strings. 
// (will be a UUID or something like it)
type EntityID string

// An AccountID is just an EntityID.
type AccountID EntityID

// Account is an object representing a User or a Group.
// (i.e. a person or an organization). this is here
// because permissions need something common
type Account struct {
  ID     AccountID
  Name   string // full name of the account
  Avatar string // a square picture for the account
  PrimaryEmail string // email associated with account
  Emails  []string // other email accounts (should include "primary email")
}

// User is an individual user, a Person.
type User struct {
  Account // it is an account. (this is "embedded here", Go uses composition instead of inheritance)

  OAuthTokens []string 
}

// Group is a group of people, an organization.
type Group struct {
  Account // it is an account.

  Owner   []AccountID
  Members Membership
}

// Conversation is a semantic grouping of entities, 
// users/groups discussing a topic, through messages
// and reference materials.
type Conversation struct {
  Entity // it is an entity

  Title string
  Owner []AccountID
  Participants Membership
}

// it would be nice to leverage some "media schema" that
// already exists rather than invent our own. It's likely
// something that the Linked Data community has already 
// made
var MediaTypes = []string{
  "publication", // paper
  "webpage",
  "image",
  ...
}

// A MediaObject is a specific piece of media that's
// able to be viewed, referenced, and linked to. 
// For example, a Paper, a webpage, or an image.
type MediaObject struct {
  Entity // it is an entity

  // all media objects must have
  Title     string
  Type      string // one of MediaTypes
  Authors   []string
  SourceURL string // the source URL of the object.

  // everything else (like DOI, etc) we can put in a big 
  // "metadata" sub-object (this just means an arbitrary json object)
  Metadata  map[string]interface{}
}

// A Note is a piece of text related to a conversation.
type Note struct {
  Entity // it is an entity

  Text         string
  Author       AccountID
  Conversation EntityID // link
}

// An Annotation is a piece of media affixed to another piece of media.
type Annotaton struct {
  Entity // it is an entity

  Source EntityID // a MediaObject
  Author AccountID // author of the annotation
}

// Type of permission. (this could be an int but may be )
type PermType string

var (
  ReadPerm PermType = "read"
  WritePerm PermType = "write"
  SharePerm PermType = "share"
)

// Membership is a group of users that belong to an entity,
// with associated permissions. For example, the participants
// of a conversation, or the members of a group.
type Membership map[AccountID]Permission

// links
type Link struct {
  Src  EntityID 
  Dst  EntityID
  Type string
}

// set of lower level functions.

// newID creates a new universal and uniformly distributed identifier.
// we can't just use the hash of the data, because we'd like to link
// to things even if the data changes.
//
// UUID is not secure: it is guessable, so since links may be used
// in "anyone with the link" type permissions, we'd like an unguessable
// identifier. 
//
// So, we'll use hash functions. to preserve upgradeability, we'll use
// multihash -- https://github.com/jbenet/multihash -- and settle on using
// sha256-256 (i.e. 256 at 256 bits.
func newID() string {

  // we first read some cryptographically secure randomness.
  // (node and browsers have ways to do this easily)
  randbuf := rand.Read(256) // read 256 bits of randomness

  // then hash that-- sometimes RNGs are owned.
  // (see https://github.com/jbenet/node-multihashing/)
  hash := multihashing.digest(randbuf, 'sha256')

  // store them as base32 for printability.
  id := base32.encode(hash)
  return id
}

// newUser makes and stores a new user account.
func newUser(name, avatar, email string) (User, error) {
  // validate name
  // validate avatar
  // validate email

  // FIRST, check another user by that email doesn't exist.
  // WARNING: this requires consistency semantics. this is so
  // common that couchdb probably has a way of handling it.
  // (probably some sort of insert-or-query)
  users := db.query({
    Type: "User",
    Emails: []string{email}, // has email
  }) // check if user email in use
  if len(users) > 0 {
    return nil, "email already in use"
  }

  u := User{
    ID: newID(),
    Name: name,
    Avatar: avatar, // have some sane default
    PrimaryEmail: email,
    Emails: []string{email},
  }

  // let's talk about elsewhere how to put to the db, globals are not great,
  // we just do it here for simplicity.
  db.put(u) // save it.
  return u
}

// newConversation makes and stores a new conversation item.
func newConversation(author User, title string) Conversation {
  // validate author 
  // validate title

  c := Conversation{
    ID: newID(),
    Title: title,
    Owner: []string{author.ID},
  }

  db.put(c) // save it.
  return c
}

// newNote makes a new note and stores it.
func newNote(author User, conv Conversation, text string) Note {
  // validate author
  // validate conv
  // validate text

  n := Note{
    ID: newID(),
    Text: text,
    Author: author.ID,
    Conversation: conv.ID,
    Participants: Membership{
      author.ID: SharePerm, // put the author as a participant too.
    },
  }
  db.put(n)
  return n
}

// StartBlankConversation is what we do when users want to start a conversation
// from scratch, unassociated with media. that is, it's not coming from a paper,
// or an anotation, or anything. it's just a conversation from scratch.
//
// we need to:
// - user u1 provides title, and note text (we'll make a first note, too)
// - create a new conversation c1 (with title, linked to u1)
// - save the conversation
// - create a new note n1 (linked to c1)
func StartBlankConversation(author User, title string, noteText string) (Conversation) {

  // make new conversation, linked to author and with title.
  c := newConversation(author, title)

  // make a new note, linked to author, linked to conversation, and with noteText.
  n := newNote(author, conversation, noteText)

  return c, n
}

// GetConversationPosts returns all the Notes, and MediaObjects related to a given conversation
// these should be sorted chronologically
func GetConversationPosts(c Conversation) []*Entity {

  notes := db.query({
    Type: "Note", 
    Conversation: c.ID,
  })

  // TODO: expand to include other media

  // TODO: order logically (chronologically)

  return notes
}

// GetConverationsForUser queries the database for all conversations that a user is a part of.
// This includes conversations the user created
func GetConversationsForUser(user User) ([]Conversation) {

  convs := db.query({
    Type: "Conversation",
    Membership: {
      author.ID: // any Perm.
    }
  })

  return convs
}

// PostToConversation adds a note to a conversation
// TODO: generalize to include other things to link in (papers, etc)
func PostToConversation(author User, conv Conversation, noteText string) (Note, error) {
  // validate author
  // validate conv
  // validate noteText

  if !permHigher(conv.Participants[User.ID], "write") {
    return nil, "no permission to post"
  }

  // make a new note, linked to author, linked to conversation, and with noteText.
  n := newNote(author, conv, noteText)

  // TODO: notifications to other participants

  return n
}

func permHigher(a, b PermType) bool {
  // check "read", "write", "share" matches.
}
RichardLitt commented 9 years ago

@jbenet Can you use the file js/data/schema.js instead? I can't comment on any particular lines here, and I'm having to go through line by line manually to find diffs.

(this is "embedded here", Go uses composition instead of inheritance): I understand that you're more familiar with Go, but building a database that uses Go principles doesn't help my implementation of it. I'm not sure what this comment means for me: should I look up Go's implementation of types? How should I structure the user account object?

I'm not sure my going through and trying to create a Node schema from this Go schema is efficient, mostly because I'm not familiar enough with prototypical Node backend architecture to make the transforms necessary from your Go syntax. The naming of fields isn't hard, but I don't know what to do about ids a lot of the time.

RichardLitt commented 9 years ago

Closing this in favor of https://github.com/BeagleLab/beagle/issues/181. Please comment there, as there are diffs of the file being edited and it should be much easier to get closer to the schema itself.