lux-framework
Version:
Build scalable, Node.js-powered REST APIs with almost no code.
609 lines (505 loc) • 16.9 kB
JavaScript
// @flow
import * as faker from 'faker';
import { expect } from 'chai';
import { it, describe, before, beforeEach, afterEach } from 'mocha';
import { dasherize, underscore } from 'inflection';
import Serializer from '../index';
import { VERSION as JSONAPI_VERSION } from '../../jsonapi';
import range from '../../../utils/range';
import { getTestApp } from '../../../../test/utils/get-test-app';
import type Application from '../../application';
import type { Model } from '../../database';
import type {
JSONAPI$DocumentLinks,
JSONAPI$ResourceObject,
JSONAPI$IdentifierObject
} from '../../jsonapi';
const DOMAIN = 'http://localhost:4000';
const linkFor = (type, id) => (
id ? `${DOMAIN}/${type}/${id}` : `${DOMAIN}/${type}`
);
describe('module "serializer"', () => {
describe('class Serializer', () => {
let subject;
let createPost;
let createSerializer;
const instances = new Set();
const setup = () => {
subject = createSerializer();
};
const teardown = () => subject.model.transaction(async trx => {
const promises = Array
.from(instances)
.map(record => record.transacting(trx).destroy());
await Promise.all(promises);
});
before(async () => {
const { models } = await getTestApp();
const Tag = models.get('tag');
const Post = models.get('post');
const User = models.get('user');
const Image = models.get('image');
const Comment = models.get('comment');
const Categorization = models.get('categorization');
if (!Post) {
throw new Error('Could not find model "Post".');
}
class TestSerializer extends Serializer {
attributes = [
'body',
'title',
'isPublic',
'createdAt',
'updatedAt'
];
hasOne = [
'user',
'image'
];
hasMany = [
'comments',
'tags'
];
}
createSerializer = (namespace = '') => new TestSerializer({
namespace,
model: Post,
parent: null
});
createPost = async ({
includeUser = true,
includeTags = true,
includeImage = true,
includeComments = true
} = {}, transaction) => {
let include = [];
const run = async trx => {
const post = await Post.transacting(trx).create({
body: faker.lorem.paragraphs(),
title: faker.lorem.sentence(),
isPublic: faker.random.boolean()
});
const postId = post.getPrimaryKey();
if (includeUser) {
// $FlowIgnore
const user = await User.transacting(trx).create({
name: `${faker.name.firstName()} ${faker.name.lastName()}`,
email: faker.internet.email(),
password: faker.internet.password(8)
});
instances.add(user);
include = [...include, 'user'];
Reflect.set(post, 'user', user);
}
if (includeImage) {
// $FlowIgnore
const image = await Image.transacting(trx).create({
postId,
url: faker.image.imageUrl()
});
instances.add(image);
include = [...include, 'image'];
}
if (includeTags) {
const tags = await Promise.all([
// $FlowIgnore
Tag.transacting(trx).create({
name: faker.lorem.word()
}),
// $FlowIgnore
Tag.transacting(trx).create({
name: faker.lorem.word()
}),
// $FlowIgnore
Tag.transacting(trx).create({
name: faker.lorem.word()
})
]);
const categorizations = await Promise.all(
tags.map(tag => (
// $FlowIgnore
Categorization.transacting(trx).create({
postId,
tagId: tag.getPrimaryKey()
})
))
);
tags.forEach(tag => {
instances.add(tag);
});
categorizations.forEach(categorization => {
instances.add(categorization);
});
include = [...include, 'tags'];
}
if (includeComments) {
const comments = await Promise.all([
// $FlowIgnore
Comment.transacting(trx).create({
postId,
message: faker.lorem.sentence()
}),
// $FlowIgnore
Comment.transacting(trx).create({
postId,
message: faker.lorem.sentence()
}),
// $FlowIgnore
Comment.transacting(trx).create({
postId,
message: faker.lorem.sentence()
})
]);
comments.forEach(comment => {
instances.add(comment);
});
include = [...include, 'comments'];
}
await post.transacting(trx).save();
return post;
};
if (transaction) {
return await run(transaction);
}
return await Post.transaction(run);
};
});
describe('#format()', function () {
this.timeout(20 * 1000);
beforeEach(setup);
afterEach(teardown);
const expectResourceToBeCorrect = async (
post,
result,
includeImage = true
) => {
const { attributes, relationships } = result;
const {
body,
title,
isPublic,
createdAt,
updatedAt
} = post.getAttributes(
'body',
'title',
'isPublic',
'createdAt',
'updatedAt'
);
const [
user,
tags,
image,
comments
] = await Promise.all([
Reflect.get(post, 'user'),
Reflect.get(post, 'tags'),
Reflect.get(post, 'image'),
Reflect.get(post, 'comments')
]);
const postId = post.getPrimaryKey();
const userId = user.getPrimaryKey();
const imageId = image ? image.getPrimaryKey() : null;
const tagIds = tags
.map(tag => tag.getPrimaryKey())
.map(String);
const commentIds = comments
.map(comment => comment.getPrimaryKey())
.map(String);
expect(result).to.have.property('id', `${postId}`);
expect(result).to.have.property('type', 'posts');
expect(attributes).to.be.an('object');
expect(relationships).to.be.an('object');
expect(attributes).to.have.property('body', body);
expect(attributes).to.have.property('title', title);
expect(attributes).to.have.property('is-public', isPublic);
expect(attributes).to.have.property('created-at', createdAt);
expect(attributes).to.have.property('updated-at', updatedAt);
let userLink;
if (subject.namespace) {
userLink = linkFor(`${subject.namespace}/users`, userId);
} else {
userLink = linkFor('users', userId);
}
expect(relationships).to.have.property('user').and.be.an('object');
expect(relationships.user).to.deep.equal({
data: {
id: `${userId}`,
type: 'users'
},
links: {
self: userLink
}
});
if (includeImage) {
let imageLink;
if (subject.namespace) {
imageLink = linkFor(`${subject.namespace}/images`, imageId);
} else {
imageLink = linkFor('images', imageId);
}
expect(relationships).to.have.property('image').and.be.an('object');
expect(relationships.image).to.deep.equal({
data: {
id: `${image.getPrimaryKey()}`,
type: 'images'
},
links: {
self: imageLink
}
});
} else {
expect(relationships.image).to.deep.equal({
data: null
});
}
expect(relationships)
.to.have.property('tags')
.and.have.property('data')
.and.be.an('array')
.with.lengthOf(tags.length);
relationships.tags.data.forEach(tag => {
expect(tag).to.have.property('id').and.be.oneOf(tagIds);
expect(tag).to.have.property('type').and.equal('tags');
});
expect(relationships)
.to.have.property('comments')
.and.have.property('data')
.and.be.an('array')
.with.lengthOf(comments.length);
relationships.comments.data.forEach(comment => {
expect(comment).to.have.property('id').and.be.oneOf(commentIds);
expect(comment).to.have.property('type').and.equal('comments');
});
};
it('works with a single instance of `Model`', async () => {
const post = await createPost();
const result = await subject.format({
data: post,
domain: DOMAIN,
include: [],
links: {
self: linkFor('posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi'
]);
await expectResourceToBeCorrect(post, result.data);
expect(result).to.have.property('links').and.deep.equal({
self: linkFor('posts', post.getPrimaryKey())
});
expect(result).to.have.property('jsonapi').and.deep.equal({
version: JSONAPI_VERSION
});
});
it('works with an array of `Model` instances', async function () {
this.slow(13 * 1000);
this.timeout(25 * 1000);
const posts = await subject.model.transaction(trx => (
Promise.all(
Array.from(range(1, 25)).map(() => createPost({}, trx))
)
));
const postIds = posts
.map(post => post.getPrimaryKey())
.map(String);
const result = await subject.format({
data: posts,
domain: DOMAIN,
include: [],
links: {
self: linkFor('posts')
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi'
]);
expect(result.data).to.be.an('array').with.lengthOf(posts.length);
for (let i = 0; i < result.data.length; i++) {
await expectResourceToBeCorrect(posts[i], result.data[i]);
}
expect(result).to.have.property('links').and.deep.equal({
self: linkFor('posts')
});
expect(result).to.have.property('jsonapi').and.deep.equal({
version: JSONAPI_VERSION
});
});
it('can build namespaced links', async () => {
subject = createSerializer('admin');
const post = await createPost();
const result = await subject.format({
data: post,
domain: DOMAIN,
include: [],
links: {
self: linkFor('admin/posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi'
]);
await expectResourceToBeCorrect(post, result.data);
expect(result).to.have.property('links').and.deep.equal({
self: linkFor('admin/posts', post.getPrimaryKey())
});
expect(result).to.have.property('jsonapi').and.deep.equal({
version: JSONAPI_VERSION
});
});
it('supports empty one-to-one relationships', async () => {
const post = await createPost({
includeUser: true,
includeTags: true,
includeImage: false,
includeComments: true
});
const result = await subject.format({
data: post,
domain: DOMAIN,
include: [],
links: {
self: linkFor('posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi'
]);
await expectResourceToBeCorrect(post, result.data, false);
expect(result).to.have.property('links').and.deep.equal({
self: linkFor('posts', post.getPrimaryKey())
});
expect(result).to.have.property('jsonapi').and.deep.equal({
version: JSONAPI_VERSION
});
});
it('supports including a has-one relationship', async () => {
const post = await createPost();
const image = await Reflect.get(post, 'image');
const result = await subject.format({
data: post,
domain: DOMAIN,
include: ['image'],
links: {
self: linkFor('posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi',
'included'
]);
await expectResourceToBeCorrect(post, result.data);
expect(result.included).to.be.an('array').with.lengthOf(1);
const { included: [item] } = result;
expect(item).to.have.property('id', `${image.getPrimaryKey()}`);
expect(item).to.have.property('type', 'images');
expect(item).to.have.property('attributes').and.be.an('object');
expect(item.attributes).to.have.property('url', image.url);
});
it('supports including belongs-to relationships', async () => {
const post = await createPost();
const user = await Reflect.get(post, 'user');
const result = await subject.format({
data: post,
domain: DOMAIN,
include: ['user'],
links: {
self: linkFor('posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi',
'included'
]);
await expectResourceToBeCorrect(post, result.data);
expect(result.included).to.be.an('array').with.lengthOf(1);
const { included: [item] } = result;
expect(item).to.have.property('id', `${user.getPrimaryKey()}`);
expect(item).to.have.property('type', 'users');
expect(item).to.have.property('attributes').and.be.an('object');
expect(item.attributes).to.have.property('name', user.name);
expect(item.attributes).to.have.property('email', user.email);
});
it('supports including a one-to-many relationship', async () => {
const post = await createPost();
const comments = await Reflect.get(post, 'comments');
const result = await subject.format({
data: post,
domain: DOMAIN,
include: ['comments'],
links: {
self: linkFor('posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi',
'included'
]);
await expectResourceToBeCorrect(post, result.data);
expect(result.included)
.to.be.an('array')
.with.lengthOf(comments.length);
result.included.forEach(item => {
expect(item).to.have.all.keys([
'id',
'type',
'links',
'attributes'
]);
expect(item).to.have.property('id').and.be.a('string');
expect(item).to.have.property('type', 'comments');
expect(item).to.have.property('attributes').and.be.an('object');
});
});
it('supports including a many-to-many relationship', async () => {
const post = await createPost();
const tags = await Reflect.get(post, 'tags');
const result = await subject.format({
data: post,
domain: DOMAIN,
include: ['tags'],
links: {
self: linkFor('posts', post.getPrimaryKey())
}
});
expect(result).to.have.all.keys([
'data',
'links',
'jsonapi',
'included'
]);
await expectResourceToBeCorrect(post, result.data);
expect(result.included)
.to.be.an('array')
.with.lengthOf(tags.length);
result.included.forEach(item => {
expect(item).to.have.all.keys([
'id',
'type',
'links',
'attributes'
]);
expect(item).to.have.property('id').and.be.a('string');
expect(item).to.have.property('type', 'tags');
expect(item).to.have.property('attributes').and.be.an('object');
});
});
});
});
});