Skip to content

Commit

Permalink
feat: adds ShoppingList and related models. See #5
Browse files Browse the repository at this point in the history
  • Loading branch information
aimed committed Aug 30, 2018
1 parent 2bd5036 commit db5364e
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 14 deletions.
2 changes: 1 addition & 1 deletion client/schema.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions server/src/__tests__/createTestUser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Connection } from 'typeorm';
import { Container } from 'typedi';
import { Fridge } from '../fridge/Fridge';
import { ShoppingList } from '../shoppinglist/ShoppingList';
import { User } from '../user/User';
import { getDeterministicString } from './getDeterministicString';

Expand All @@ -13,10 +14,8 @@ export async function createNewTestUser(connection: Connection = Container.get(C
const email = getDeterministicString() + '@example.com';
const password = await User.hashPassword('password');
const user = users.create({ email, password });

const fridges = connection.getRepository(Fridge);
const fridge = await fridges.save(fridges.create());
user.fridge = Promise.resolve(fridge);
user.fridge = Promise.resolve(new Fridge());
user.shoppingList = Promise.resolve(new ShoppingList());

await users.save(user);
return user;
Expand Down
2 changes: 2 additions & 0 deletions server/src/auth/Registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EmailPasswordPair } from './Authentication';
import { Fridge } from '../fridge/Fridge';
import { InjectRepository } from 'typeorm-typedi-extensions';
import { Service } from 'typedi';
import { ShoppingList } from '../shoppinglist/ShoppingList';
import { User } from '../user/User';

@Service()
Expand All @@ -27,6 +28,7 @@ export class Registration {

// Every user should own one fridge.
instance.fridge = Promise.resolve(new Fridge());
instance.shoppingList = Promise.resolve(new ShoppingList());

await this.userRepo.save(instance);
return instance;
Expand Down
2 changes: 2 additions & 0 deletions server/src/auth/__tests__/Registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ describe(Registration.name, () => {
});
expect(created).toBeTruthy();
expect(created!.email).toEqual(email);
expect(await created!.fridge).toBeTruthy();
expect(await created!.shoppingList).toBeTruthy();

const user = await userRepo.findOne({ email });
expect(user).toBeTruthy();
Expand Down
4 changes: 4 additions & 0 deletions server/src/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { FridgeIngredient } from './fridge/FridgeIngredient';
import { Ingredient } from './ingredient/Ingredient';
import { PasswordResetToken } from './auth/PasswordResetToken';
import { Role } from './user/Role';
import { ShoppingList } from './shoppinglist/ShoppingList';
import { ShoppingListItem } from './shoppinglist/ShoppingListItem';
import { User } from './user/User';

/**
Expand All @@ -19,5 +21,7 @@ export function getEntities() {
Fridge,
FridgeIngredient,
Ingredient,
ShoppingList,
ShoppingListItem,
];
}
15 changes: 9 additions & 6 deletions server/src/fridge/Fridge.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Field, ID, ObjectType } from 'type-graphql';

import { FridgeIngredient } from './FridgeIngredient';
import {
FridgeIngredientsConnection,
FridgeIngredientsConnectionEdge,
} from './FridgeIngredientsConnection';

import { FridgeIngredient } from './FridgeIngredient';

/**
* @NOTE: Having a separate fridge that is not part of the user allows us to have a separate
* resolver.
*/
@Entity()
@ObjectType()
export class Fridge {
@Field(type => ID)
@Field(() => ID)
@PrimaryGeneratedColumn()
public readonly id!: number;

@Field(type => FridgeIngredientsConnection)
@OneToMany(type => FridgeIngredient, fridgeIngredients => fridgeIngredients.fridge)
@Field(() => FridgeIngredientsConnection)
@OneToMany(() => FridgeIngredient, fridgeIngredients => fridgeIngredients.fridge)
public ingredients!: Promise<FridgeIngredient[]>;
}
4 changes: 2 additions & 2 deletions server/src/fridge/FridgeIngredientsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export function createFridgeIngredientsConnectionEdge(
) {
const edge = new FridgeIngredientsConnectionEdge();
edge.node = fridgeIngredient;
edge.cursor = [FridgeIngredientsConnectionEdge.name, fridgeIngredient.id].join('.');
edge.cursor = [FridgeIngredientsConnectionEdge.name, fridgeIngredient.id].join('_');
return edge;
}

export function getFridgeIngredientIdFromCursor(cursor: string) {
return cursor.split('.')[1];
return cursor.split('_')[1];
}
8 changes: 8 additions & 0 deletions server/src/ingredient/Ingredient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeor
import { Field, ID, ObjectType } from 'type-graphql';

import { FridgeIngredient } from '../fridge/FridgeIngredient';
import { ShoppingListItem } from '../shoppinglist/ShoppingListItem';

@Entity()
@ObjectType()
Expand All @@ -22,6 +23,13 @@ export class Ingredient {
)
public readonly fridgeIngredients!: Promise<FridgeIngredient>;

// Intentionally not readable to prevent snooping into other shopping lists.
@OneToMany(
() => ShoppingListItem,
shoppingListItem => shoppingListItem.item,
)
public readonly shoppingListItems!: Promise<ShoppingListItem>;

@Field({ nullable: true })
public readonly icon?: string;
}
2 changes: 1 addition & 1 deletion server/src/ingredient/IngredientsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export class IngredientsConnection extends Connection<Ingredient> {
export function createIngredientsConnectionEdge(ingredient: Ingredient): IngredientsConnectionEdge {
const edge = new IngredientsConnectionEdge();
edge.node = ingredient;
edge.cursor = [IngredientsConnectionEdge.name, ingredient.id].join('');
edge.cursor = [IngredientsConnectionEdge.name, ingredient.id].join('_');
return edge;
}
21 changes: 21 additions & 0 deletions server/src/shoppinglist/ShoppingList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Field, ID, ObjectType } from 'type-graphql';

import { ShoppingListItem } from './ShoppingListItem';
import { ShoppingListItemsConnection } from './ShoppingListItemsConnection';

/**
* @NOTE: Having a separate shopping list that is not a property of the user allows us to have a
* separate resolver.
*/
@Entity()
@ObjectType()
export class ShoppingList {
@Field(() => ID)
@PrimaryGeneratedColumn()
public readonly id!: number;

@Field(() => ShoppingListItemsConnection)
@OneToMany(() => ShoppingListItem, shoppingListItem => shoppingListItem.shoppingList)
public items!: Promise<ShoppingListItem[]>;
}
28 changes: 28 additions & 0 deletions server/src/shoppinglist/ShoppingListItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Field, ObjectType } from 'type-graphql';

import { Ingredient } from '../ingredient/Ingredient';
import { ShoppingList } from './ShoppingList';
import { UNITS } from '../fridge/Units';

@Entity()
@ObjectType()
export class ShoppingListItem {
@PrimaryGeneratedColumn()
public readonly id!: number;

@ManyToOne(() => ShoppingList, shoppingList => shoppingList.items)
public shoppingList!: Promise<ShoppingList>;

@Field(() => Ingredient)
@ManyToOne(() => Ingredient, ingredient => ingredient.fridgeIngredients)
public item!: Promise<Ingredient>;

@Field()
@Column({ enum: UNITS })
public unit!: string;

@Field()
@Column()
public amount!: number;
}
25 changes: 25 additions & 0 deletions server/src/shoppinglist/ShoppingListItemsConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Connection, Edge } from '../graphql/connections';
import { Field, ObjectType } from 'type-graphql';

import { ShoppingListItem } from './ShoppingListItem';

@ObjectType()
export class ShoppingListItemsConnectionEdge extends Edge<ShoppingListItem> {
@Field(() => ShoppingListItem)
public node!: ShoppingListItem;
}

@ObjectType()
export class ShoppingListItemsConnection extends Connection<ShoppingListItem> {
@Field(() => ShoppingListItemsConnectionEdge)
public edges!: Edge<ShoppingListItem>[];
}

export function createShoppingListItemsEdge(
node: ShoppingListItem,
): ShoppingListItemsConnectionEdge {
const edge = new ShoppingListItemsConnectionEdge();
edge.node = node;
edge.cursor = [ShoppingListItemsConnectionEdge.name, node.id].join('_');
return edge;
}
57 changes: 57 additions & 0 deletions server/src/shoppinglist/ShoppingListResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Args,
ArgsType,
Authorized,
Ctx,
Field,
Mutation,
ObjectType,
Resolver,
} from 'type-graphql';
import { IsIn, IsNumber, MinLength } from 'class-validator';

import { InjectRepository } from 'typeorm-typedi-extensions';
import { Repository } from 'typeorm';
import { Service } from 'typedi';
import { ShoppingListItem } from './ShoppingListItem';
import { ShoppingListItemsConnectionEdge } from './ShoppingListItemsConnection';
import { UNITS } from '../fridge/Units';
import { User } from '../user/User';

@ArgsType()
class AddItemArgs {
@Field()
@MinLength(1)
public readonly name!: string;

@Field()
@IsIn(UNITS)
public readonly unit!: string;

@Field()
@IsNumber()
public readonly amount!: number;
}

@ObjectType()
class AddItemResponse {
@Field()
public shoppingListItemsConnectionEdge!: ShoppingListItemsConnectionEdge;
}

@Service()
@Resolver()
export class ShoppingListResolver {
@InjectRepository(() => ShoppingListItem)
private readonly shoppingListItemRepo!: Repository<ShoppingListItem>;

@Mutation(() => AddItemResponse)
@Authorized()
public async addItem(
@Args() addItem: AddItemArgs,
@Ctx('user') user: User,
): Promise<AddItemResponse> {
// let ShoppingListItem;
throw new Error('Not implemented');
}
}
6 changes: 6 additions & 0 deletions server/src/user/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Fridge } from '../fridge/Fridge';
import { PasswordResetToken } from '../auth/PasswordResetToken';
import { Role } from './Role';
import { Security } from '../auth/Security';
import { ShoppingList } from '../shoppinglist/ShoppingList';

@Entity()
@ObjectType()
Expand Down Expand Up @@ -48,6 +49,11 @@ export class User {
@JoinColumn()
public fridge!: Promise<Fridge>;

@Field(type => ShoppingList)
@OneToOne(type => ShoppingList, { cascade: true, onDelete: 'CASCADE' })
@JoinColumn()
public shoppingList!: Promise<ShoppingList>;

/**
* Hashes the users password.
* @param plaintextPassword The plain text password.
Expand Down

0 comments on commit db5364e

Please sign in to comment.