Skip to content
This repository was archived by the owner on Oct 23, 2018. It is now read-only.

Add media data to entries #43

Merged
merged 7 commits into from
Oct 21, 2018
Merged
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
14 changes: 4 additions & 10 deletions database/scripts/create.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ CREATE TABLE users
image text
);

CREATE TYPE media_type AS ENUM ('game', 'anime', 'show', 'movie');
CREATE TYPE media_type AS ENUM ('game', 'anime', 'tv', 'movie');

CREATE TABLE list
(
Expand All @@ -18,21 +18,15 @@ CREATE TABLE list
name text not null
);

CREATE TABLE media
(
id serial primary key not null,
api_id text not null
);

CREATE TABLE entry
(
id serial primary key not null,
media_id int not null REFERENCES media,
media_id int not null,
list_id int not null REFERENCES list ON DELETE CASCADE,
category text,
tags text[] DEFAULT '{}',
tags text[] not null DEFAULT '{}',
rating int,
last_updated TIMESTAMP,
last_updated TIMESTAMP DEFAULT now(),
-- started/finished use ISO 8601 with partial dates allowed
-- e.g. '2016', '2017-04-13', '2018-09'
started text,
Expand Down
14 changes: 4 additions & 10 deletions database/scripts/testData.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,14 @@ VALUES
('user3', '$2a$10$xYR4Kdp9/eCHpW8p15Jov.u7RHXVLKxQER4VrPirtYkPTR2aOh6rq', '[email protected]'),
('user4', '$2a$10$xYR4Kdp9/eCHpW8p15Jov.u7RHXVLKxQER4VrPirtYkPTR2aOh6rq', '[email protected]');

-- Media
INSERT INTO media(api_id)
VALUES
('12345'),
('67890');

INSERT INTO list(name, user_id, media_type)
VALUES
('mezzode''s List', 1, 'game');

-- Entry
INSERT INTO entry(media_id, category, started, finished, list_id, last_updated, tags)
VALUES
(1, 'Progress', '2016', '2018', 1, now(), '{"Favourites", "Friends"}'),
(2, 'Complete', '2017-10-01', '2017-10-01', 1, now(), '{"Favourites"}'),
(1, 'Progress', '2017-10-01', '2017-10-01', 1, now(), null),
(2, 'Complete', '2017-10', '2017-10-01', 1, now(), null);
(12579, 'Progress', '2016', '2018', 1, now(), '{"Favourites", "Friends"}'),
(9694, 'Complete', '2017-10-01', '2017-10-01', 1, now(), '{"Favourites"}'),
(12579, 'Progress', '2017-10-01', '2017-10-01', 1, now(), '{}'),
(9694, 'Complete', '2017-10', '2017-10-01', 1, now(), '{}');
2 changes: 1 addition & 1 deletion server/src/helpers/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const bodyCodesToIds: RequestHandler = (req, res, next) => {
next();
};

const defaultFields = ['Entry', 'Media', 'List'];
const defaultFields = ['Entry', 'List'];

/**
* Replaces codes with ids in a given object.
Expand Down
14 changes: 14 additions & 0 deletions server/src/routes/api/mediaapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import igdb from 'igdb-api-node';
import { DateTime } from 'luxon';
import * as fetch from 'node-fetch';
import { MediaType } from '../lists/types';
import * as queries from './queries';
import { Anime, Game, Movie, SearchResults, Subset, TV } from './types';

Expand Down Expand Up @@ -265,3 +266,16 @@ export async function animeFetchSearch(
console.log(final);
return final;
}

export const fetchMedia = (id: number, mediaType: MediaType) => {
switch (mediaType) {
case MediaType.Anime:
return animeFetchID(id);
case MediaType.Game:
return gameFetchID(id);
case MediaType.Movie:
return movietvFetchID(id, MovieTvType.Movie);
case MediaType.Show:
return movietvFetchID(id, MovieTvType.TV);
}
};
4 changes: 3 additions & 1 deletion server/src/routes/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ export interface Subset {
mediaType: string;
}

export type Results = Game | Movie | TV | Anime;

export interface Game {
id: number;
title: string;
status: string;
description: string;
genres: string[];
cover: string | null;
category: string[];
category: string;
themes: string[];
publishers: string[];
developers: string[];
Expand Down
49 changes: 9 additions & 40 deletions server/src/routes/lists/entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ describe('Test entries endpoints', () => {

describe('Test entry create', () => {
test('Can create new entry', async () => {
const entry = {
const entryData = {
category: 'In Progress',
finished: null,
listCode: 'XG', // 1
mediaCode: 'XG', // 1
rating: 10,
started: '2018',
tags: [],
};
const entry = {
...entryData,
mediaId: 1,
};
const res = await request(app)
.post('/entry')
.send(entry)
Expand All @@ -42,40 +45,12 @@ describe('Test entries endpoints', () => {

const { body } = res;
expect(body).toBeDefined();
const { media, lastUpdated, ...createdEntry } = body;

const { entryCode } = body;
const [entryId] = hashids.decode(entryCode);
expect(entryId).toBeGreaterThan(0);
expect(body).toEqual({ ...entry, entryCode });
});

test("Can't create entry for non-existent media", async () => {
const errSpy = jest.spyOn(console, 'error');
errSpy.mockImplementation();

const entry = {
category: 'In Progress',
finished: null,
listCode: 'XG', // 1
mediaCode: 'BaDCoDe',
rating: 10,
started: '2018',
tags: [],
};

const res = await request(app)
.post('/entry')
.send(entry)
.set('Accept', 'application/json')
.expect(404);

const error = 'Media not found';

const { body } = res;
expect(body).toEqual({ error });

expect(errSpy).toBeCalledWith(new HandlerError(error, 404));
errSpy.mockRestore();
expect(createdEntry).toEqual({ ...entryData, entryCode });
});

test("Can't create entry for non-existent list", async () => {
Expand Down Expand Up @@ -144,18 +119,12 @@ describe('Test entries endpoints', () => {

expect(res.body).toBeDefined();

const { lastUpdated, ...body } = res.body;
expect(body).toEqual({
const { lastUpdated, media, ...entry } = res.body;
expect(entry).toEqual({
category: 'In Progress',
entryCode: 'XG',
finished: '2018',
listCode: 'XG',
media: {
artUrl:
'https://78.media.tumblr.com/4f30940e947b58fb57e2b8499f460acb/tumblr_okccrbpkDY1rb48exo1_1280.jpg',
mediaCode: 'XG',
title: `Title of media ID 1`,
},
rating: 9,
started: '2016',
tags: [],
Expand Down
160 changes: 90 additions & 70 deletions server/src/routes/lists/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import {
idsToCodes,
paramCodesToIds,
} from '../../helpers/id';
import { UserEntry } from './types';
import { fetchMedia } from '../api/mediaapi';
import { Entry, MediaType, UserEntry } from './types';

export const entryFields = `
id AS "entryId",
last_updated AS "lastUpdated",
category,
tags,
rating,
started,
finished,
media_id AS "mediaId",
list_id AS "listId"
`;
e.id AS "entryId",
e.last_updated AS "lastUpdated",
e.category,
e.tags,
e.rating,
e.started,
e.finished,
e.media_id AS "mediaId",
e.list_id AS "listId"`;

const getEntry = asyncHandler(async (req, res) => {
const { entryId }: { entryId: number } = req.params;
Expand All @@ -35,27 +35,23 @@ const getEntry = asyncHandler(async (req, res) => {
started: string;
finished: string;
tags: string;
mediaType: MediaType;
}>(
// TODO: separate the select clause so can reuse (e.g. same as in lists.ts)
`SELECT ${entryFields}
`SELECT ${entryFields}, l.media_type AS "mediaType"
FROM entry e
JOIN list l ON l.id = e.list_id
WHERE e.id = $(entryId)`,
{ entryId },
);
if (!row) {
throw new HandlerError('Entry not found', 404);
}

const { mediaId, ...other } = row;
const { mediaId, mediaType, ...other } = row;
const entry = {
...idsToCodes(other),
media: {
// TODO: get media data
artUrl:
'https://78.media.tumblr.com/4f30940e947b58fb57e2b8499f460acb/tumblr_okccrbpkDY1rb48exo1_1280.jpg',
mediaCode: hashids.encode(mediaId),
title: `Title of media ID ${mediaId}`,
},
media: await fetchMedia(mediaId, mediaType),
};

res.send(entry);
Expand Down Expand Up @@ -83,27 +79,35 @@ const newEntry = asyncHandler(async (req, res) => {
...data,
};

// TODO: consider checking for existence of list first so can
// return "List not found" instead of generic error
const insertedEntry = await db.task<Entry>(async t => {
// TODO: consider checking for existence of list first so can
// return "List not found" instead of generic error

// note this returns list_id, etc. in snake case
const { entryId, ...insertedData } = await db.one<
{ entryId: number } & UserEntry
>(
`${pgp.helpers.insert(entry, undefined, 'entry')}
RETURNING $(data:name), id AS "entryId"`,
{ entry, data },
);
const entryCode = hashids.encode(entryId);
const listCode = hashids.encode(listId);
const mediaCode = hashids.encode(mediaId);

const insertedEntry = {
entryCode,
listCode,
mediaCode,
...insertedData,
};
const { mediaType } = await t.one<{ mediaType: MediaType }>(
`SELECT media_type AS "mediaType"
FROM list l
WHERE l.id = $(listId)`,
{ listId },
);

// note this returns list_id, etc. in snake case
const { entryId, ...insertedData } = await t.one<
{ entryId: number; lastUpdated: string } & UserEntry
>(
`${pgp.helpers.insert(entry, undefined, 'entry')}
RETURNING $(data:name), id AS "entryId", last_updated AS "lastUpdated"`,
{ entry, data },
);
const entryCode = hashids.encode(entryId);
const listCode = hashids.encode(listId);

return {
entryCode,
listCode,
...insertedData,
media: await fetchMedia(mediaId, mediaType),
};
});

res.json(insertedEntry);
});
Expand Down Expand Up @@ -162,37 +166,53 @@ const updateEntry = asyncHandler(async (req, res) => {
throw new HandlerError('Invalid entry', 400);
}
const { entryId } = req.params;
const { mediaId, ...updatedEntry } = await db.one<{
entryId: number;
mediaId: number;
listId: number;
category: string;
tags: string[];
rating: number;
lastUpdated: string;
started: string;
finished: string;
last_updated: string;
}>(
`${pgp.helpers.update(
{ ...entryUpdate, last_updated: DateTime.local() },
undefined,
'entry',
)}
WHERE id = $(entryId)
RETURNING ${entryFields}`,
{ entryId, entryUpdate },
);
res.json({
...idsToCodes(updatedEntry),
media: {
// TODO: get media data
artUrl:
'https://78.media.tumblr.com/4f30940e947b58fb57e2b8499f460acb/tumblr_okccrbpkDY1rb48exo1_1280.jpg',
mediaCode: hashids.encode(mediaId),
title: `Title of media ID ${mediaId}`,
},

const editedEntry = await db.task<Entry>(async t => {
const { mediaId, ...updatedEntry } = await t.one<{
entryId: number;
mediaId: number;
listId: number;
category: string;
tags: string[];
rating: number;
lastUpdated: string;
started: string;
finished: string;
last_updated: string;
}>(
`${pgp.helpers.update(
{ ...entryUpdate, last_updated: DateTime.local() },
undefined,
'entry',
)}
WHERE id = $(entryId)
RETURNING
id AS "entryId",
last_updated AS "lastUpdated",
category,
tags,
rating,
started,
finished,
media_id AS "mediaId",
list_id AS "listId"`,
{ entryId, entryUpdate },
);

const { mediaType } = await t.one<{ mediaType: MediaType }>(
`SELECT media_type AS "mediaType"
FROM list l
JOIN entry e ON e.id = $(entryId) AND e.list_id = l.id`,
{ entryId },
);

return {
...(idsToCodes(updatedEntry) as Entry),
media: await fetchMedia(mediaId, mediaType),
};
});

res.json(editedEntry);
});

const deleteEntry = asyncHandler(async (req, res) => {
Expand Down
Loading