Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions examples/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { append, map, foldl } from 'funcadelic';
import { NumberType, StringType } from './types';
import { create as _create } from '../index';
import { link, valueOf, pathOf, atomOf, typeOf, metaOf, ownerOf } from '../src/meta';
import Txn from '../src/transaction';
import belongsTo from './db/belongs-to';
import hasMany from './db/has-many';

import faker from 'faker';

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

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

class Comment {
title = StringType;
}

function Id(record) {
return ln(StringType, pathOf(record).concat("id"), record);
}

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 path = pathOf(this).concat(["records", id]);
let record = Txn(this.nextId.increment(), ln(Type, path))
.flatMap(([, record]) => Txn(Id(record).set(id)));

return foldl((txn, { key, value: attr }) => {
return txn
.flatMap(txn => {
let [ record ] = txn;
if (record[key]) {
return txn
.flatMap(([ record ]) => Txn(record[key], ln(DB, pathOf(this).slice(0, -1))))
.flatMap(([ relationship, db ]) => {

function attrFn() {
if (relationship.isBelongsTo || relationship.isHasMany) {
let build = factory[key];
if (build) {
let empty = relationship.isHasMany ? [] : {};
return build(relationship, db, overrides[key] || empty);
} else {
return null;
}
} else {
return typeof attr === 'function' ? attr(id) : attr;
}
}

let result = attrFn();

if (metaOf(result)) {
return Txn(result, record);
} else {
return Txn(relationship.set(result), record);
}
})
.flatMap(([, record]) => Txn(record));
} else {
return txn;
}
});
}, record, append(factory, overrides));
}
};
}

class DB {
people = Table(Person, {
firstName: () => faker.name.firstName(),
lastName: () => faker.name.lastName()
});
blogs = Table(Blog, {
title: () => faker.random.words(),
author: (author, db, attrs = {}) => Txn(db.people)
.flatMap(([ people ]) => Txn(people.create(attrs).latest, author))
.flatMap(([ person, author ]) => Txn(author.set(person))),
comments: (comments, db, list = []) => list.reduce((txn, attrs) => {
return txn
.flatMap(([ db ]) => Txn(db.comments.create(attrs), comments))
.flatMap(([ db, comments ]) => Txn(comments.push(db.comments.latest), db))
.flatMap(([, db ]) => Txn(db));
}, Txn(db))
});

comments = Table(Comment);
}

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

// function DB(tables) {
// return class DB {
// constructor() {
// Object.keys(tables).forEach(key => {
// this[key] = tables[key]
// });
// }
// }
// }

export default _create(DB, {});
36 changes: 36 additions & 0 deletions examples/db/belongs-to.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { StringType } from '../types';
import Relationship from '../../src/relationship';
import { view, At } from '../../src/lens';
import { valueOf } from '../../src/meta';
import linkTo from './link-to';
import { ln } from '../db';

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

function BelongsTo(originType, originPath, foreignKey) {

return class BelongsTo extends T {
static name = `BelongsTo<${T.name}>`;

get isBelongsTo() { return true; }

set(record) {
let path = originPath.concat(foreignKey);
return ln(StringType, path, this).set(idOf(record));
}
};
}

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 function idOf(record) {
return view(At("id"), valueOf(record));
}
46 changes: 46 additions & 0 deletions examples/db/has-many.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Relationship from '../../src/relationship';
import linkTo, { expandPath } from './link-to';
import { ln } from '../db';
import { atomOf, pathOf, valueOf } from '../../src/meta';
import { idOf } from './belongs-to';

export default function hasMany(T, tableName) {
return new Relationship(resolve);

function resolve(origin, originType, originPath, relationshipName) {
let dbpath = expandPath(["..", "..", ".."], originPath);
let Type = HasMany(T, dbpath, tableName);
let { resolve } = linkTo(Type, [relationshipName]);
return resolve(origin, originType, originPath, relationshipName);
}
}

export function HasMany(Type, dbpath, tableName) {
return class HasMany {
static name = `HasMany<${Type.name}>`;

get isHasMany() { return true; }

get length() {
return (valueOf(this) || []).length;
}

push(record) {
let value = valueOf(this) || [];
return this.set(value.concat(idOf(record)));
}

*[Symbol.iterator]() {
let ids = valueOf(this) || [];
for (let id of ids) {
let path = dbpath.concat([tableName, "records", id]);
yield ln(Type, path, this);
}
}
};
}

export function collect(db, tableName) {
let Type = HasMany(db[tableName].constructor.Type, pathOf(db), tableName);
return ln(Type, atomOf(db));
}
24 changes: 24 additions & 0 deletions examples/db/link-to.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Relationship from '../../src/relationship';

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

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

let target = expandPath(path, originPath);

return { Type, path: target };
}
}

export function expandPath(path, context) {
return path.reduce((path, element) => {
if (element === '..') {
return path.slice(0, -1);
} else if (element === '.') {
return path;
} else {
return path.concat(element);
}
}, context);
}
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';
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@
},
"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",
Expand Down
9 changes: 6 additions & 3 deletions src/cached-property.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { stable } from 'funcadelic';

export default function CachedProperty(key, reify) {

let get = stable(object => reify(object));

let enumerable = true;
let configurable = true;
return {
enumerable,
configurable,
get() {
let value = reify(this);
Object.defineProperty(this, key, { enumerable, value });
return value;
return get(this);
}
};
}
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
43 changes: 43 additions & 0 deletions src/transaction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { view, set } from './lens';
import { Meta, atomOf } from './meta';

export default function transaction(...args) {
return new Transaction(...args);
}

/**
* This strict monadic API is awkward to work with, but it does guarante that
* everything will be respected.
*/

class Transaction {

constructor(subject, ...members) {
this.atom = atomOf(subject);
this.subject = prune(subject);
this.members = members.map(member => set(Meta.atom, this.atom, prune(member)));
return set(Meta.atom, this.atom, this);
}

flatMap(fn) {
let result = fn(this);
if (result instanceof Transaction) {
return result;
} else {
throw new Error('in Transaction#flatMap(fn), `fn` should return a Transaction, but returned a ' + result);
}
}

log(...msgs) {
console.log(...msgs, JSON.stringify(atomOf(this), null, 2));
return this;
}

*[Symbol.iterator]() {
yield* [this.subject].concat(this.members);
}
}

function prune(object) {
return set(Meta.owner, view(Meta.location, object), object);
}
Loading