Skip to content

Commit

Permalink
Add an example of a foreign key relationship.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cowboyd committed Dec 11, 2018
1 parent 1a91ddf commit ae45efd
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 6 deletions.
142 changes: 142 additions & 0 deletions examples/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { append, map, foldl } from 'funcadelic';
import { NumberType, StringType } from './types';
import { create as _create } from '../index';
import { view, At } from '../src/lens';
import { link, valueOf, pathOf, atomOf, typeOf, metaOf, ownerOf } from '../src/meta';

import faker from 'faker';

class Person {
firstName = StringType;
lastName = StringType;
}

class Blog {
title = StringType;
author = belongsTo(Person, "people");
}

class Comment {}

function Table(Type, factory = {}) {

return class Table {
static Type = Type;
static name = `Table<${Type.name}>`;

nextId = NumberType;

get length() { return Object.keys(this.records).length; }

get latest() { return this.records[this.latestId]; }

get latestId() { return this.nextId.state - 1; }

get records() {
return map((value, key) => ln(Type, pathOf(this).concat(["records", key]), this), this.state.records);
}

get state() {
return valueOf(this) || { nextId: 0, records: {}};
}

create(overrides = {}) {
let id = this.nextId.state;
let record = createAt(this.nextId.increment(), Type, ["records", id], { id });

let created = foldl((record, { key, value: attr }) => {
if (record[key]) {
let attrFn = typeof attr === 'function' ? attr : () => attr;

// create a local link of the DB that returns itself. to pass into
// the factory function.
let db = link(_create(DB), DB, pathOf(this).slice(0, -1), atomOf(record));

let result = attrFn(overrides[key], db);

let next = metaOf(result) ? link(record, Type, pathOf(record), atomOf(result)) : record;

return next[key].set(result);
} else {
return record;
}
}, record, append(factory, overrides));

let owner = ownerOf(this);
return link(this, typeOf(this), pathOf(this), atomOf(created), owner.Type, owner.path);
}
};
}



class DB {
people = Table(Person, {
firstName: () => faker.name.firstName(),
lastName: () => faker.name.lastName()
});
blogs = Table(Blog, {
title: () => faker.random.words(),
author: (attrs, db) => {
return db.people.create(attrs)
.people.latest;
}
});
comments = Table(Comment);
}

function createAt(parent, Type, path, value) {
return link(_create(Type), Type, pathOf(parent).concat(path), atomOf(parent)).set(value);
}

function ln(Type, path, owner) {
return link(_create(Type), Type, path, atomOf(owner), typeOf(owner), pathOf(owner));
}

import Relationship from '../src/relationship';

function linkTo(Type, path) {
return new Relationship(resolve);

function resolve(origin, originType, originPath /*, relationshipName */) {

return {
Type,
path: path.reduce((path, element) => {
if (element === '..') {
return path.slice(0, -1);
} else if (element === '.') {
return path;
} else {
return path.concat(element);
}
}, originPath)
};
}
}

function belongsTo(T, tableName) {
return new Relationship(resolve);

function BelongsTo(originType, originPath, foreignKey) {
return class BelongsTo extends T {
set(value) {
let origin = ln(originType, originPath, value);
let id = valueOf(value).id;
return origin.set(append(valueOf(origin), {
[foreignKey]: id
}));
}
};
}

function resolve(origin, originType, originPath, relationshipName) {
let foreignKey = `${relationshipName}Id`;
let id = view(At(foreignKey), valueOf(origin));
let Type = BelongsTo(originType, originPath, foreignKey);
let { resolve } = linkTo(Type, ["..", "..", "..", tableName, "records", id]);
return resolve(origin, originType, originPath, relationshipName);
}
}

export default _create(DB, {});
28 changes: 28 additions & 0 deletions examples/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { valueOf } from '../index';

export class NumberType {

get state() {
return valueOf(this) || 0;
}

initialize(value) {
if (value == null) {
return 0;
} else if (isNaN(value)) {
return this;
} else {
return Number(value);
}
}

increment() {
return this.state + 1;
}
}

export class StringType {
get state() {
return valueOf(this) || '';
}
}
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import create from './src/create';
import Identity from './src/identity';

export { create, Identity };
export { valueOf, metaOf, atomOf } from './src/meta';
export { valueOf, metaOf, atomOf, pathOf } from './src/meta';
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,23 @@
},
"devDependencies": {
"@babel/core": "7.1.6",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/register": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"babel-eslint": "^10.0.1",
"coveralls": "3.0.2",
"eslint": "^5.7.0",
"eslint-plugin-prefer-let": "^1.0.1",
"expect": "^23.4.0",
"faker": "^4.1.0",
"invariant": "^2.2.4",
"mocha": "^5.2.0",
"nyc": "13.1.0",
"rollup": "^0.67.4",
"rollup-plugin-babel": "4.0.3",
"rollup-plugin-filesize": "5.0.1",
"rollup-plugin-node-resolve": "3.4.0",
"@babel/plugin-proposal-class-properties": "^7.1.0"
"rollup-plugin-node-resolve": "3.4.0"
},
"dependencies": {
"funcadelic": "^0.5.4"
Expand Down
6 changes: 5 additions & 1 deletion src/meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ class Location {

export const AtomOf = type(class AtomOf {
atomOf(object) {
return this(object).atomOf(object);
if (object != null) {
return this(object).atomOf(object);
} else {
return undefined;
}
}
});

Expand Down
62 changes: 62 additions & 0 deletions tests/db.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import expect from 'expect';

import db from '../examples/db';
import { valueOf } from '../index';

describe.only('a referential DB', ()=> {
it('starts out empty', ()=> {
expect(db.people.length).toEqual(0);
expect(db.blogs.length).toEqual(0);
expect(db.comments.length).toEqual(0);
});

describe('creating a person with static attributes', ()=> {
let next;
let person;
beforeEach(()=> {
next = db.people.create({
firstName: 'Bob',
lastName: 'Dobalina'
});
person = next.people.latest;
});
it('contains the newly created person', ()=> {
expect(next.people.length).toEqual(1);
expect(person).toBeDefined();
expect(person.firstName.state).toEqual('Bob');
expect(person.lastName.state).toEqual('Dobalina');
});
});

describe('creating a person with higher order attributes', ()=> {
let next;
let person;
beforeEach(()=> {
next = db.people.create();
person = next.people.latest;
});

it('creates them with generated attributes', ()=> {
expect(person.firstName.state).not.toBe('');
expect(person.lastName.state).not.toBe('');
});
});

describe('creating a blog post with related author', ()=> {
let next;
let blog;

beforeEach(()=> {
next = db.blogs.create();
blog = next.blogs.latest;
});
it('has a related author', ()=> {
expect(blog.author).toBeDefined();
});
it('is the same as a person created in the people table', ()=> {
expect(next.people.latest).toBeDefined();
expect(valueOf(next.people.latest)).toBe(valueOf(next.blogs.latest.author));
});
});

});
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,11 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=

faker@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=

fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
Expand Down Expand Up @@ -1889,7 +1894,7 @@ inquirer@^6.1.0:
strip-ansi "^4.0.0"
through "^2.3.6"

[email protected], invariant@^2.2.2:
[email protected], invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
Expand Down

0 comments on commit ae45efd

Please sign in to comment.