mobxjs / mobx-state-tree

Full-featured reactive state management without the boilerplate
https://mobx-state-tree.js.org/
MIT License
6.94k stars 641 forks source link

Question: Normalizing data structure #235

Closed elado closed 7 years ago

elado commented 7 years ago

I want to have a normalized db store, including relationships.

{
  books: {
    'b1': {
      id: 'b1', title: 'book 1',
      author /* or authorId: */: 'a1',
      categories /* or categoryIds: */: ['c1', 'c2'],
    },
  },
  authors: {
    'a1': { id: 'a1', name: 'author 1' },
  },
  categories: {
    'c1': { id: 'c1', name: 'c1' },
    'c2': { id: 'c2', name: 'c2' },
  },
}

This is straightforward to build, but inserting the data may become complex.

If the server I'm talking to returns:

[
  {
    id: 'b1', title: 'b1',
    author: { id: 'a1', name: 'a1' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c2', name: 'c2' } ]
  },
  {
    id: 'b2', title: 'b2',
    author: { id: 'a2', name: 'a2' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c3', name: 'c3' } ]
  },
  ...
]

My Book model has a author: types.reference(Author) and categories: types.array(Category), just inserting a Book into a collection doesn't insert author and categories to the right places in the tree.

Ideally I could throw entities in some structure at the store and it'll upsert them by type and ID, walk through the reference properties and do that recursively.

Is there anything like that in MST, or anything planned?

cpunion commented 7 years ago

Yay, I just wrote a demo, not simple.

import React from "react"
import { types, getSnapshot } from "mobx-state-tree"
import { observer } from "mobx-react"
import { inspect, render } from "mobx-state-tree-playground"

const Author = types.model('Author', {
    id: types.identifier(),
    name: types.string
})

const Category = types.model('Category', {
    id: types.identifier(),
    name: types.string
})

const Book = types.model('Book', {
    id: types.identifier(),
    title: types.string,
    author: types.reference(Author),
    categories: types.array(types.reference(Category))
})

const AppModel = types.model({
    books: types.map(Book),
    authors: types.map(Author),
    categories: types.map(Category)
}, {
    addAuthor(authorData) {
        const author = Author.create(authorData)
        this.authors.put(author)
    },
    addBook(bookData) {
        const book = Book.create(bookData)
        this.books.put(book)
    },
    addCategory(categoryData) {
        const category = Category.create(categoryData)
        this.categories.put(category)
    }
})

const store = AppModel.create({books: {}, authors: {}, categories: {}})
inspect(store)

//////////////////////// Convert function
function convertStore(store, data) {
    const AuthorData = types.model('Author', {
        id: types.identifier(),
        name: types.string
    }, {
        afterAttach() {
            store.addAuthor(getSnapshot(this))
        }
    })

    const CategoryData = types.model('Category', {
        id: types.identifier(),
        name: types.string
    }, {
        afterAttach() {
            store.addCategory(getSnapshot(this))
        }
    })

    const BookData = types.model('Book', {
        id: types.identifier(),
        title: types.string,
        author: AuthorData,
        categories: types.array(CategoryData)
    }, {
        afterAttach() {
            const bookData = getSnapshot(this)
            store.addBook(Object.assign({}, bookData, {
                author: this.author.id,
                categories: this.categories.map(c => c.id)
            }))
        }
    })

    const DataStore = types.model('DataStore', {
        books: types.array(BookData)
    })

    DataStore.create({books: data})
}

//////////////////// Test
const data = [
  {
    id: 'b1', title: 'b1',
    author: { id: 'a1', name: 'a1' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c2', name: 'c2' } ]
  },
  {
    id: 'b2', title: 'b2',
    author: { id: 'a2', name: 'a2' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c3', name: 'c3' } ]
  }
]

convertStore(store, data)

See in Playground%0A%0Aconst%20Category%20%3D%20types.model('Category'%2C%20%7B%0A%20%20%20%20id%3A%20types.identifier()%2C%0A%20%20%20%20name%3A%20types.string%0A%7D)%0A%0Aconst%20Book%20%3D%20types.model('Book'%2C%20%7B%0A%20%20%20%20id%3A%20types.identifier()%2C%0A%20%20%20%20title%3A%20types.string%2C%0A%20%20%20%20author%3A%20types.reference(Author)%2C%0A%20%20%20%20categories%3A%20types.array(types.reference(Category))%0A%7D)%0A%0Aconst%20AppModel%20%3D%20types.model(%7B%0A%20%20%20%20books%3A%20types.map(Book)%2C%0A%20%20%20%20authors%3A%20types.map(Author)%2C%0A%20%20%20%20categories%3A%20types.map(Category)%0A%7D%2C%20%7B%0A%20%20%20%20addAuthor(authorData)%20%7B%0A%20%20%20%20%20%20%20%20const%20author%20%3D%20Author.create(authorData)%0A%20%20%20%20%20%20%20%20this.authors.put(author)%0A%20%20%20%20%7D%2C%0A%20%20%20%20addBook(bookData)%20%7B%0A%20%20%20%20%20%20%20%20const%20book%20%3D%20Book.create(bookData)%0A%20%20%20%20%20%20%20%20this.books.put(book)%0A%20%20%20%20%7D%2C%0A%20%20%20%20addCategory(categoryData)%20%7B%0A%20%20%20%20%20%20%20%20const%20category%20%3D%20Category.create(categoryData)%0A%20%20%20%20%20%20%20%20this.categories.put(category)%0A%20%20%20%20%7D%0A%7D)%0A%0Aconst%20store%20%3D%20AppModel.create(%7Bbooks%3A%20%7B%7D%2C%20authors%3A%20%7B%7D%2C%20categories%3A%20%7B%7D%7D)%0Ainspect(store)%0A%0A%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%20Convert%20function%0Afunction%20convertStore(store%2C%20data)%20%7B%0A%20%20%20%20const%20AuthorData%20%3D%20types.model('Author'%2C%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20types.identifier()%2C%0A%20%20%20%20%20%20%20%20name%3A%20types.string%0A%20%20%20%20%7D%2C%20%7B%0A%20%20%20%20%20%20%20%20afterAttach()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20store.addAuthor(getSnapshot(this))%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%0A%0A%20%20%20%20const%20CategoryData%20%3D%20types.model('Category'%2C%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20types.identifier()%2C%0A%20%20%20%20%20%20%20%20name%3A%20types.string%0A%20%20%20%20%7D%2C%20%7B%0A%20%20%20%20%20%20%20%20afterAttach()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20store.addCategory(getSnapshot(this))%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%0A%0A%20%20%20%20const%20BookData%20%3D%20types.model('Book'%2C%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20types.identifier()%2C%0A%20%20%20%20%20%20%20%20title%3A%20types.string%2C%0A%20%20%20%20%20%20%20%20author%3A%20AuthorData%2C%0A%20%20%20%20%20%20%20%20categories%3A%20types.array(CategoryData)%0A%20%20%20%20%7D%2C%20%7B%0A%20%20%20%20%20%20%20%20afterAttach()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20bookData%20%3D%20getSnapshot(this)%0A%20%20%20%20%20%20%20%20%20%20%20%20store.addBook(Object.assign(%7B%7D%2C%20bookData%2C%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20author%3A%20this.author.id%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20categories%3A%20this.categories.map(c%20%3D%3E%20c.id)%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D))%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%0A%0A%20%20%20%20const%20DataStore%20%3D%20types.model('DataStore'%2C%20%7B%0A%20%20%20%20%20%20%20%20books%3A%20types.array(BookData)%0A%20%20%20%20%7D)%0A%0A%20%20%20%20DataStore.create(%7Bbooks%3A%20data%7D)%0A%7D%0A%0A%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%20Test%0Aconst%20data%20%3D%20%5B%0A%20%20%7B%0A%20%20%20%20id%3A%20'b1'%2C%20title%3A%20'b1'%2C%0A%20%20%20%20author%3A%20%7B%20id%3A%20'a1'%2C%20name%3A%20'a1'%20%7D%2C%0A%20%20%20%20categories%3A%20%5B%20%7B%20id%3A%20'c1'%2C%20name%3A%20'c1'%20%7D%2C%20%7B%20id%3A%20'c2'%2C%20name%3A%20'c2'%20%7D%20%5D%0A%20%20%7D%2C%0A%20%20%7B%0A%20%20%20%20id%3A%20'b2'%2C%20title%3A%20'b2'%2C%0A%20%20%20%20author%3A%20%7B%20id%3A%20'a2'%2C%20name%3A%20'a2'%20%7D%2C%0A%20%20%20%20categories%3A%20%5B%20%7B%20id%3A%20'c1'%2C%20name%3A%20'c1'%20%7D%2C%20%7B%20id%3A%20'c3'%2C%20name%3A%20'c3'%20%7D%20%5D%0A%20%20%7D%0A%5D%0A%0AconvertStore(store%2C%20data)%0A)

mattiamanzati commented 7 years ago

There is no included solution yet, but for a low boilerplate solution you can check out normalizr and use it :)

    addFetchedData(data){
      const author = new schema.Entity('authors')
      const category = new schema.Entity('categories')
      const book = new schema.Entity('books', {
        author,
        categories: [category]
      })

      const {entities} = normalize(data, [book])
      this.authors.merge(entities.authors)
      this.categories.merge(entities.categories)
      this.books.merge(entities.books)
    }

https://codesandbox.io/s/X64kr4QRm

cpunion commented 7 years ago

Wow! Nearly perfect for me!

Thanks!

elado commented 7 years ago

Thank you so much! normalizr is indeed where I was aiming. Would be interesting to write a wrapper that translates MST models to normalizr, so all of that is seamless. I'll explore this direction. Closing for now :)