Skip to content

Commit 1a91ddf

Browse files
committed
Abstract Relationships
We've introduced a very, very low-level linking functionality to have a microstate draw its value from anywhere in the tree. However, what's lacking is a way to declaratively state from within the microstate what those links _are_ and where they go. So, for example in the BigTest network ORM, we want to be able to say: ```js import { belongsTo } from '@bigtest/network'; class Blog { title = String; author = belongsTo(Person); } ``` and for it to link to the correct person. To accomplish this, we introduce the concept of a relationship which is just an abstraction over a Type and a path. Every reference from one microstate to another is now reconceived as a relatioship. It's not that complicated though. A relationship is really just a function that takes the holder object, it's type, it's path, and the name of the relationship and returns a `{ Type, path }` pair that is used for linking. For example, the default relationship is `Child` and is defined as: ```js function Child(spec) { return new Relationship(resolve); function resolve(origin, originType, path, name) { let Type = typeof spec === 'function' ? spec : typeOf(spec); return { Type, path: path.concat(name) }; } } ``` It's what implements the first line DSL. `belongsTo`, which resolves to a different place in the store, could be implemented like so: ```js function belongsTo(Type) { return new Relationship(resolve); function resolve(object, objectType, path, name) { let tableName = pluralize(Type.name.toLowerCase()); let id = valueOf(this)[`${name}Id`]; return { Type, path: path.slice(-3).concat([tableName, "records", id]) }; } } ``` this assumes a layout of the DB like: ```json { blogs: { nextId: 2, records: { 0: { title: 'How to blog', authorId: 'id1' }, 1: { title: 'How to build a fully immutable directed cyclic graph in javascript', authorId: '0' } } }, people: { nextId: 1, records: { 0: { firstName: 'Charles', lastName: 'Lowell' } } } } ``` So you can see that what we're really doing is using a relative path within the DB. The expression ```js path.slice(-3).concat([tableName, "records", id]) ``` is really equivalent to `["..", "..", "..", tableName, "records", id]` if we were writing it out using a more classic method for expressing paths. ** Follow on Work / Questions ** - Need a way to specify what happens when you _set_ a relationship. For example, in the case of `belongsTo`, you need to update the `authorId` atribute at the appropriate path. We might be able to use some sort of lens here. - This will mess with Identity. Specifically, if a linked object that isn't actually contained within this object changes, then the identity of the object needs to change (I think). For example, if I say: ```js blog.author.firstName.set('Carlos') ``` even though the `valueOf(blog)` will not have changed (it still has `{authorId: 0}`). It should be represented by a new object because. This will be possible because of laziness.
1 parent 1e680d5 commit 1a91ddf

File tree

7 files changed

+77
-76
lines changed

7 files changed

+77
-76
lines changed

src/create.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { set } from './lens';
2-
import { link, mount, locationOf, metaOf, atomOf, ownerOf, valueOf } from './meta';
2+
import { link, locationOf, metaOf, atomOf, ownerOf, typeOf } from './meta';
33
import MicrostateType from './microstate-type';
4+
import Relationship from './relationship';
45

56
export default function create(Type, value) {
67
let Microstate = MicrostateType(Type, transition, property);
@@ -23,9 +24,18 @@ function transition(object, Type, path, name, method, ...args) {
2324
return link(create(owner.Type), owner.Type, owner.path, patch());
2425
}
2526

26-
function property(object, Type, path, name, currentValue) {
27-
let value = valueOf(object);
28-
let expanded = typeof currentValue === 'function' ? create(currentValue, value) : currentValue;
29-
let substate = value != null && value[name] != null ? expanded.set(value[name]) : expanded;
30-
return mount(object, substate, name);
27+
function property(object, Type, path, name, relationship) {
28+
let { resolve } = (relationship instanceof Relationship ? relationship : Child(relationship));
29+
let target = resolve(object, Type, path, name);
30+
let owner = ownerOf(object);
31+
return link(create(target.Type), target.Type, target.path, atomOf(object), owner.Type, owner.path);
32+
}
33+
34+
function Child(spec) {
35+
return new Relationship(resolve);
36+
37+
function resolve(origin, originType, path, name) {
38+
let Type = typeof spec === 'function' ? spec : typeOf(spec);
39+
return { Type, path: path.concat(name) };
40+
}
3141
}

src/identity.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export default function Identity(microstate, fn) {
7272
return fn(Type, name, path, args);
7373
}
7474

75-
function propertyFn(object, Type, path, /* name, currentValue */) {
76-
let location = compose(Path(path), Id.ref);
75+
function propertyFn(object, Type, path, name /* relationship */) {
76+
let location = compose(Path(path.concat(name)), Id.ref);
7777
return view(location, paths);
7878
}
7979
}

src/meta.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type } from 'funcadelic';
22

3-
import { At, view, set, over, compose, Path } from './lens';
3+
import { At, view, set, compose, Path } from './lens';
44

55
export function root(microstate, Type, value) {
66
return set(Meta.data, new Meta(new Location(Type, []), value), microstate);
@@ -10,10 +10,6 @@ export function link(object, Type, path, atom, Owner = Type, ownerPath = path) {
1010
return set(Meta.data, new Meta(new Location(Type, path), atom, new Location(Owner, ownerPath)), object);
1111
}
1212

13-
export function mount(microstate, substate, key) {
14-
return over(Meta.data, meta => meta.mount(microstate, key), substate);
15-
}
16-
1713
export function valueOf(object) {
1814
let { lens } = view(Meta.location, object);
1915
if (lens != null) {
@@ -57,11 +53,6 @@ export class Meta {
5753
this.location = location;
5854
this.owner = owner;
5955
}
60-
61-
mount(onto, atKey) {
62-
let location = new Location(this.location.Type, pathOf(onto).concat(atKey));
63-
return new Meta(location, atomOf(onto), ownerOf(onto));
64-
}
6556
}
6657

6758
class Location {

src/microstate-type.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function MicrostateType(Type, transitionFn, propertyFn) {
1212
constructor(value) {
1313
super(value);
1414
Object.defineProperties(this, map((slot, key) => {
15-
return CachedProperty(key, self => propertyFn(self, Type, pathOf(this).concat(key), key, slot));
15+
return CachedProperty(key, self => propertyFn(self, Type, pathOf(self), key, slot));
1616
}, this));
1717
return root(this, Type, value);
1818
}

src/relationship.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default class Relationship {
2+
constructor(resolve) {
3+
this.resolve = resolve;
4+
}
5+
6+
resolve(/*origin, Type, path, name */) {}
7+
}

tests/lab.test.js

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { append } from 'funcadelic';
2-
import { create, valueOf, atomOf } from '../index';
3-
import { mount, link } from '../src/meta';
1+
import { create, valueOf } from '../index';
42
import expect from 'expect';
53

64
describe('Lab', () => {
@@ -118,58 +116,4 @@ describe('Lab', () => {
118116
});
119117

120118
});
121-
122-
describe('A globally linked microstate within another microstate', () => {
123-
class Person {
124-
get left() {
125-
return mount(this, append(create(Hand), {
126-
get other() {
127-
return link(create(Hand), Hand, ['right'], atomOf(this), Hand, ['left']);
128-
}
129-
}), 'left');
130-
}
131-
get right() {
132-
return mount(this, append(create(Hand), {
133-
get other() {
134-
return link(create(Hand), Hand, ['left'], atomOf(this), Hand, ['right']);
135-
}
136-
}), 'right');
137-
}
138-
}
139-
140-
class Hand {
141-
other = Hand;
142-
claps = Num;
143-
144-
clap() {
145-
return this
146-
.other.claps.increment()
147-
.claps.increment();
148-
}
149-
}
150-
151-
let person;
152-
beforeEach(function() {
153-
person = create(Person, { left: { claps: 1 }, right: { claps: 1 } });
154-
});
155-
156-
it('links the proper states', function() {
157-
expect(person.left.claps.state).toEqual(1);
158-
expect(person.right.claps.state).toEqual(1);
159-
});
160-
161-
describe('transitioning', function() {
162-
163-
let clapped;
164-
beforeEach(function() {
165-
clapped = person.right.clap();
166-
});
167-
it('claps both hands', function() {
168-
expect(clapped.left.claps.state).toEqual(2);
169-
expect(clapped.right.claps.state).toEqual(2);
170-
});
171-
});
172-
173-
})
174-
;
175119
});

tests/relationship.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import expect from 'expect';
2+
import Relationship from '../src/relationship';
3+
import { valueOf } from '../src/meta';
4+
import create from '../src/create';
5+
6+
describe('abstract relationships', ()=> {
7+
class A {
8+
b = class B {
9+
c = class C {
10+
a = root(A);
11+
}
12+
}
13+
}
14+
15+
function root(Type) {
16+
return new Relationship(() => ({ Type, path: []}));
17+
}
18+
19+
let a;
20+
let atom;
21+
22+
beforeEach(()=> {
23+
atom = { b: { c: 'Hallo' } };
24+
a = create(A, atom);
25+
});
26+
27+
28+
it('allows perfectly circular data structures', ()=> {
29+
expect(valueOf(a.b.c.a)).toBe(valueOf(a));
30+
expect(valueOf(a.b.c.a.b.c.a)).toBe(valueOf(a));
31+
expect(valueOf(a.b.c.a.b.c.a.b.c.a)).toBe(valueOf(a));
32+
expect(valueOf(a.b.c.a.b.c.a.b.c.a.b.c.a)).toBe(valueOf(a));
33+
});
34+
35+
describe('transitioning from deep within the circular structure', ()=> {
36+
let next;
37+
beforeEach(()=> {
38+
next = a.b.c.a.b.c.a.b.c.a.b.c.a.b.c.set('bye now!');
39+
});
40+
it('sets the right value', ()=> {
41+
expect(valueOf(next)).toEqual({
42+
b: {
43+
c: 'bye now!'
44+
}
45+
});
46+
});
47+
});
48+
49+
});

0 commit comments

Comments
 (0)