Skip to content

Commit 84fd6ec

Browse files
committed
Add an example of a foreign key relationship.
This adds a `DB` example that contains a number of tables each of which have rows that link to each other. This uses the ability to link anywhere in the store to create a factory method that creates entities elswhere in the store. In this example, the `Blog` record has a reference to a `Person` which is the author. The factory for author attribute of a blog, creates that person in the database, and then returns that person.
1 parent 19ffae1 commit 84fd6ec

File tree

7 files changed

+248
-4
lines changed

7 files changed

+248
-4
lines changed

examples/db.js

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { append, map, foldl } from 'funcadelic';
2+
import { NumberType, StringType } from './types';
3+
import { create as _create } from '../index';
4+
import { view, At } from '../src/lens';
5+
import { link, valueOf, pathOf, atomOf, typeOf, metaOf, ownerOf } from '../src/meta';
6+
7+
import faker from 'faker';
8+
9+
class Person {
10+
firstName = StringType;
11+
lastName = StringType;
12+
}
13+
14+
class Blog {
15+
title = StringType;
16+
author = belongsTo(Person, "people");
17+
}
18+
19+
class Comment {}
20+
21+
function Table(Type, factory = {}) {
22+
23+
return class Table {
24+
static Type = Type;
25+
static name = `Table<${Type.name}>`;
26+
27+
nextId = NumberType;
28+
29+
get length() { return Object.keys(this.records).length; }
30+
31+
get latest() { return this.records[this.latestId]; }
32+
33+
get latestId() { return this.nextId.state - 1; }
34+
35+
get records() {
36+
return map((value, key) => ln(Type, pathOf(this).concat(["records", key]), this), this.state.records);
37+
}
38+
39+
get state() {
40+
return valueOf(this) || { nextId: 0, records: {}};
41+
}
42+
43+
create(overrides = {}) {
44+
let id = this.nextId.state;
45+
let record = createAt(this.nextId.increment(), Type, ["records", id], { id });
46+
47+
let created = foldl((record, { key, value: attr }) => {
48+
if (record[key]) {
49+
let attrFn = typeof attr === 'function' ? attr : () => attr;
50+
51+
// create a local link of the DB that returns itself. to pass into
52+
// the factory function.
53+
let db = link(_create(DB), DB, pathOf(this).slice(0, -1), atomOf(record));
54+
55+
let result = attrFn(overrides[key], db);
56+
57+
let next = metaOf(result) ? link(record, Type, pathOf(record), atomOf(result)) : record;
58+
59+
return next[key].set(result);
60+
} else {
61+
return record;
62+
}
63+
}, record, append(factory, overrides));
64+
65+
let owner = ownerOf(this);
66+
return link(this, typeOf(this), pathOf(this), atomOf(created), owner.Type, owner.path);
67+
}
68+
};
69+
}
70+
71+
72+
73+
class DB {
74+
people = Table(Person, {
75+
firstName: () => faker.name.firstName(),
76+
lastName: () => faker.name.lastName()
77+
});
78+
blogs = Table(Blog, {
79+
title: () => faker.random.words(),
80+
author: (attrs, db) => {
81+
return db.people.create(attrs)
82+
.people.latest;
83+
}
84+
});
85+
comments = Table(Comment);
86+
}
87+
88+
function createAt(parent, Type, path, value) {
89+
return link(_create(Type), Type, pathOf(parent).concat(path), atomOf(parent)).set(value);
90+
}
91+
92+
function ln(Type, path, owner) {
93+
return link(_create(Type), Type, path, atomOf(owner), typeOf(owner), pathOf(owner));
94+
}
95+
96+
import Relationship from '../src/relationship';
97+
98+
function linkTo(Type, path) {
99+
return new Relationship(resolve);
100+
101+
function resolve(origin, originType, originPath /*, relationshipName */) {
102+
103+
return {
104+
Type,
105+
path: path.reduce((path, element) => {
106+
if (element === '..') {
107+
return path.slice(0, -1);
108+
} else if (element === '.') {
109+
return path;
110+
} else {
111+
return path.concat(element);
112+
}
113+
}, originPath)
114+
};
115+
}
116+
}
117+
118+
function belongsTo(T, tableName) {
119+
return new Relationship(resolve);
120+
121+
function BelongsTo(originType, originPath, foreignKey) {
122+
return class BelongsTo extends T {
123+
set(value) {
124+
let origin = ln(originType, originPath, value);
125+
let id = valueOf(value).id;
126+
return origin.set(append(valueOf(origin), {
127+
[foreignKey]: id
128+
}));
129+
}
130+
};
131+
}
132+
133+
function resolve(origin, originType, originPath, relationshipName) {
134+
let foreignKey = `${relationshipName}Id`;
135+
let id = view(At(foreignKey), valueOf(origin));
136+
let Type = BelongsTo(originType, originPath, foreignKey);
137+
let { resolve } = linkTo(Type, ["..", "..", "..", tableName, "records", id]);
138+
return resolve(origin, originType, originPath, relationshipName);
139+
}
140+
}
141+
142+
export default _create(DB, {});

examples/types.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { valueOf } from '../index';
2+
3+
export class NumberType {
4+
5+
get state() {
6+
return valueOf(this) || 0;
7+
}
8+
9+
initialize(value) {
10+
if (value == null) {
11+
return 0;
12+
} else if (isNaN(value)) {
13+
return this;
14+
} else {
15+
return Number(value);
16+
}
17+
}
18+
19+
increment() {
20+
return this.state + 1;
21+
}
22+
}
23+
24+
export class StringType {
25+
get state() {
26+
return valueOf(this) || '';
27+
}
28+
}

index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ import create from './src/create';
22
import Identity from './src/identity';
33

44
export { create, Identity };
5-
export { valueOf, metaOf, atomOf } from './src/meta';
5+
export { valueOf, metaOf, atomOf, pathOf } from './src/meta';

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@
2626
},
2727
"devDependencies": {
2828
"@babel/core": "7.1.6",
29+
"@babel/plugin-proposal-class-properties": "^7.1.0",
30+
"@babel/polyfill": "^7.0.0",
2931
"@babel/preset-env": "^7.0.0",
3032
"@babel/register": "^7.0.0",
31-
"@babel/polyfill": "^7.0.0",
3233
"babel-eslint": "^10.0.1",
3334
"coveralls": "3.0.2",
3435
"eslint": "^5.7.0",
3536
"eslint-plugin-prefer-let": "^1.0.1",
3637
"expect": "^23.4.0",
38+
"faker": "^4.1.0",
39+
"invariant": "^2.2.4",
3740
"mocha": "^5.2.0",
3841
"nyc": "13.1.0",
3942
"rollup": "^0.67.4",

src/meta.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ class Location {
6868

6969
export const AtomOf = type(class AtomOf {
7070
atomOf(object) {
71-
return this(object).atomOf(object);
71+
if (object != null) {
72+
return this(object).atomOf(object);
73+
} else {
74+
return undefined;
75+
}
7276
}
7377
});
7478

tests/db.test.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import expect from 'expect';
2+
3+
import db from '../examples/db';
4+
import { valueOf } from '../index';
5+
6+
describe('a referential DB', ()=> {
7+
it('starts out empty', ()=> {
8+
expect(db.people.length).toEqual(0);
9+
expect(db.blogs.length).toEqual(0);
10+
expect(db.comments.length).toEqual(0);
11+
});
12+
13+
describe('creating a person with static attributes', ()=> {
14+
let next;
15+
let person;
16+
beforeEach(()=> {
17+
next = db.people.create({
18+
firstName: 'Bob',
19+
lastName: 'Dobalina'
20+
});
21+
person = next.people.latest;
22+
});
23+
it('contains the newly created person', ()=> {
24+
expect(next.people.length).toEqual(1);
25+
expect(person).toBeDefined();
26+
expect(person.firstName.state).toEqual('Bob');
27+
expect(person.lastName.state).toEqual('Dobalina');
28+
});
29+
});
30+
31+
describe('creating a person with higher order attributes', ()=> {
32+
let next;
33+
let person;
34+
beforeEach(()=> {
35+
next = db.people.create();
36+
person = next.people.latest;
37+
});
38+
39+
it('creates them with generated attributes', ()=> {
40+
expect(person.firstName.state).not.toBe('');
41+
expect(person.lastName.state).not.toBe('');
42+
});
43+
});
44+
45+
describe('creating a blog post with related author', ()=> {
46+
let next;
47+
let blog;
48+
49+
beforeEach(()=> {
50+
next = db.blogs.create();
51+
blog = next.blogs.latest;
52+
});
53+
it('has a related author', ()=> {
54+
expect(blog.author).toBeDefined();
55+
});
56+
it('is the same as a person created in the people table', ()=> {
57+
expect(next.people.latest).toBeDefined();
58+
expect(valueOf(next.people.latest)).toBe(valueOf(next.blogs.latest.author));
59+
});
60+
});
61+
62+
});

yarn.lock

+6-1
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,11 @@ extsprintf@^1.2.0:
15031503
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
15041504
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
15051505

1506+
faker@^4.1.0:
1507+
version "4.1.0"
1508+
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
1509+
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=
1510+
15061511
fast-deep-equal@^1.0.0:
15071512
version "1.1.0"
15081513
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@@ -1889,7 +1894,7 @@ inquirer@^6.1.0:
18891894
strip-ansi "^4.0.0"
18901895
through "^2.3.6"
18911896

1892-
[email protected], invariant@^2.2.2:
1897+
[email protected], invariant@^2.2.2, invariant@^2.2.4:
18931898
version "2.2.4"
18941899
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
18951900
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==

0 commit comments

Comments
 (0)