@kamranayub
background by saragnzalez
handles
provides a declarative way to render your UI that separates business logic from rendering logic
Horst Rittel and Melvin Webber defined a "wicked" problem as one that could be clearly defined only by solving it, or by solving part of it. This paradox implies, essentially, that you have to "solve" the problem once in order to clearly define it and then solve it again to create a solution that works.
Code Complete, Steve McConnell
[
{},
{},
{}
]
[
{
id: "product_title",
display_name: "Product Title"
},
{
id: "street_date",
display_name: "Street Date"
},
{
id: "genres",
display_name: "Genres"
}
]
[
{
id: "product_title",
display_name: "Product Title",
component_type: "single_line_text"
},
{
id: "street_date",
display_name: "Street Date",
component_type: "date"
},
{
id: "genres",
display_name: "Genres",
component_type: "multi_select"
}
]
[
{
id: "product_title",
display_name: "Product Title",
component_type: "single_line_text",
min_length: 0,
max_length: 1337
},
{
id: "street_date",
display_name: "Street Date",
component_type: "date",
min_date: 1569861733,
max_date: null
},
{
id: "genres",
display_name: "Genres",
component_type: "multi_select",
min_values: 1,
max_values: 3
}
]
{
id: "product_title",
display_name: "Product Title",
component_type: "single_line_text",
notes: [
"Not editable by VENDOR role"
]
}
{
id: "street_date",
display_name: "Street Date",
component_type: "date",
notes: [
"Must be today or future"
]
}
{
id: "product_title",
display_name: "Product Title",
component_type: "single_line_text",
min_length: 0,
max_length: 1337,
notes: [
"Not editable by VENDOR role"
]
}
{
id: "product_title",
display_name: "Product Title",
component_type: "single_line_text",
min_length: 0,
max_length: 1337,
read_only: false,
notes: [
"For VENDOR role, READ_ONLY is TRUE"
]
}
item
product_title
street_date
{
"product_title": "Pokemon Sword",
"street_date": "2019-11-15T00:00:00Z"
}
This is a "consumer" way of thinking
product_title
rules
display
values
street_date
rules
display
values
{
"product_title": {
"display_name": "Product Title",
"description": "A product's title",
"default_rules": {
"required": true,
"min_length": 1,
"max_length": 1337,
"read_only": false
},
"display": {
"component_type": "single_line_text",
"help_text": null
},
"item_values": [
{
"id": "item1",
"value_raw": "Pokemon Sword",
"value_human": "Pokemon Sword"
},
{
"id": "item2",
"value_raw": "Pokemon Sword (Digital Edition)",
"value_human": "Pokemon Sword (Digital Edition)",
"override_rules": {
"required": false,
"read_only": true
}
}
]
},
"street_date": {
"display_name": "Street Date",
"description": "The date this item will be available for purchase",
"default_rules": {
"required": true,
"min_date": 1555818103157,
"max_date": null,
},
"display": {
"component_type": "date",
"help_text": "Must be later than today's date"
},
"item_values": [
{
"id": "item1",
"value_raw": "2019-11-15T00:00:00Z",
"value_human": "Nov 15, 2019",
"override_rules": {
"min_date": null
}
}
]
}
}
primary
data
external data
caching layer
GraphQL Backend
schema
engine
data
resolution
React Frontend
state
management
client rule engine
which provides
type ItemAttribute {
id: String!
display: AttributeDisplay
}
type AttributeDisplay {
component_type: ComponentType!
}
enum ComponentType {
single_line_text
multi_line_text
number
date
datetime
multi_option
}
Static typing
Powerful query capabilities
query data($attribute_ids: [String!]!, $item_ids: [String!]!, hydrate: Boolean!) {
attributes(ids: $attribute_ids) {
id
title: description
default_rules {
max_length
}
values(items: $item_ids) {
raw_value
human_value @include(if: $hydrate)
override_rules {
max_length
}
}
}
item_metadata(ids: $item_ids) {
last_updated
}
}
type AttributeRules {
required: Boolean
read_only: Boolean
max_length: Int
max_chars: Int @deprecated(reason: "Use `max_length` instead")
}
One version, field-level reporting
subscription item_updates(ids: ["item1"]) {
item_id
values {
raw_value
human_value
}
}
Subscriptions
Caching infrastructure
Field-level timing and analytics
Custom directives
Extensible architecture
you could totally use REST too
query {
itemData(attribute_ids: ["genres"]) {
attributes {
id
default_rules {
required
read_only
}
display {
component_type
}
values(item_ids: ["item1", "item2"]) {
item_id
raw_value
human_value
override_rules {
required
}
}
}
}
}
Load raw data
Load external data
Map the attributes
Mapping the attributes
genres
export default {
id: "genres",
async fetchData(context) {
const { itemIds, services } = context;
// fetch from db
const itemData = await services.db.getItems(itemIds);
return itemData.map(data => ({
item_id: data.id,
raw_value: data.genres.map(genre => genre.genre_id);
}))
}
}
Fetching raw and human data
genres
Database
[1, 3]
External
APIs
["Adventure", "Role-Playing Game"]
Fetching raw and human data
genres
title
Database
per-request cache
genres
platforms
brands
publisher
...
Fetching raw and human data
Caching layer
Service 1
Service 2
{
id: "genres",
default_rules: {
required: true,
read_only: false
},
display: {
component_type: "multi_select"
},
values: [
{
item_id: "item1",
raw_value: 2,
human_value: "Adventure",
override_rules: null
}
]
}
Merge in values
Run rules
Tweak schema
Finalize
Base schema
Base schema
{
"product_title": {
"display_name": "Product Title",
"description": "A product's title",
"default_rules": {
"required": true,
"min_length": 1,
"max_length": 1337,
"read_only": false
},
"display": {
"component_type": "single_line_text",
"help_text": null
}
},
"street_date": {
"display_name": "Street Date",
"description": "The date this item will be available for purchase",
"default_rules": {
"required": true,
"min_date": 1555818103157,
"max_date": null,
},
"display": {
"component_type": "date",
"help_text": "Must be later than today's date"
}
}
}
{
id: "genres",
default_rules: {
required: true,
read_only: false
},
display: {
component_type: "multi_select"
},
values: [
{
item_id: "item1",
raw_value: 2,
human_value: "Adventure",
override_rules: null
}
]
}
{
id: "genres",
default_rules: {
required: true,
read_only: false
},
display: {
component_type: "multi_select"
},
values: [
{
item_id: "item1",
raw_value: 2,
human_value: "Adventure",
override_rules: {
required: false,
read_only: true
}
}
]
}
Business rules run on the server and are represented in simple terms to the client
primary
data
external data
caching layer
GraphQL Backend
schema
engine
data
resolution
React Frontend
state
management
client rule engine
{
"attributes": [
{
"id": "product_title",
"display": {
"component_type": "single_line_text"
}
}
]
}
import * as Components from './componentTypes';
function renderField(componentType, props) {
if (Components[componentType]) {
return React.createElement(
Components[componentType],
props
);
}
throw new Error(`Unknown component type: ${componentType}`)
}
{
"attributes": [
{
"id": "product_title",
"default_rules": {
"read_only": true
},
"display": {
"component_type": "single_line_text"
},
"item_values": [
{
"id": 1,
"raw_value": "Pokemon Sword",
"human_value": "Pokemon Sword"
}
]
}
]
}
function SingleLineText(props) {
const { read_only, raw_value, human_value } = props;
const [value, setValue] = React.useState(raw_value);
const handleChange = React.useCallback(e => {
setValue(e.target.value);
});
if (read_only) {
return (<div>{human_value}</div>);
}
return (
<input type="text" value={value} onChange={handleChange} />
)
}
{
"attributes": [
{
"id": "product_title",
"default_rules": {
"read_only": true,
"max_length": 50
},
"display": {
"component_type": "single_line_text"
}
}
]
}
{
"attributes": [
{
"id": "genres",
"display": {
"component_type": "multi_select"
}
}
]
}
query {
lookup(attribute_id: "genres") {
page_size
total
options(filter: "act") {
raw_value
human_value
}
}
}
Action
Time
Action
Action
Change
Change
{
"item1": {
"product_title": [
{
"raw_value": "version 3", "human_value": "version 3"
},
{
"raw_value": "version 2", "human_value": "version 2"
}
]
}
}
Attribute
Group
"Editor"
e.g. product_title
e.g. dimensions (WxHxD)
e.g. complex attributes
Graph theory and math comes in handy!
{
"id": "primary_genre",
"relationships": [
{ "type": "hierarchy", "related_id": "genres" }
]
}
{
"id": "genres",
"relationships": [
{ "type": "blocking", "related_id": "product_title" }
]
}
{
"id": "number_of_players",
"relationships": [
{ "type": "conditional", "related_id": "genres", "when": [1], "match": "some" }
]
}
{
product_title: [
{ item_ids: ["item1", "item2"], value: "Pokemon Sword" },
{ item_ids: ["item3"], value: "Pokemon Sword (Digital Edition)" }
],
street_date: [
{ item_ids: ["item1", "item2", "item3"], value: "2019-11-15T00:00:00Z" }
]
}
Send
Compare
Validate
Persist
Publish
The save flow reuses much of the same schema infrastructure
First, decide whether this is something you'll find value from. Do you have dozens or hundreds of data points to support?
If it doesn't, then roll your own schema
Things I didn't talk about:
State management
Testing
Universal rule engine
@kamranayub