Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement HasAndBelongsToMany relation #6452

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import {
BelongsToAccessor,
BelongsToDefinition,
createBelongsToAccessor,
createHasAndBelongsToManyRepositoryFactory,
createHasManyRepositoryFactory,
createHasManyThroughRepositoryFactory,
DefaultCrudRepository,
Entity,
EntityCrudRepository,
EntityNotFoundError,
Getter,
HasAndBelongsToManyDefinition,
HasAndBelongsToManyRepository,
HasAndBelongsToManyRepositoryFactory,
HasManyDefinition,
HasManyRepository,
HasManyRepositoryFactory,
Expand Down Expand Up @@ -472,6 +476,248 @@ describe('HasManyThrough relation', () => {
}
});

describe('HasAndBelongsToMany relation', () => {
let existingCustomerId: number;
// Customer has many CartItems through CustomerCartItemLink
let customerCartItemRepo: HasAndBelongsToManyRepository<
CartItem,
typeof CartItem.prototype.id
>;
let customerCartItemFactory: HasAndBelongsToManyRepositoryFactory<
CartItem,
typeof CartItem.prototype.id,
typeof Customer.prototype.id
>;

before(givenCrudRepositories);
before(givenPersistedCustomerInstance);
before(givenConstrainedRepositories);

beforeEach(async function resetDatabase() {
await customerRepo.deleteAll();
await customerCartItemLinkRepo.deleteAll();
await cartItemRepo.deleteAll();
});

it('creates a target instance along with the corresponding through model', async () => {
const cartItem = await customerCartItemRepo.create({
description: 'an item hasManyThrough',
});
const persistedItem = await cartItemRepo.findById(cartItem.id);
const persistedLink = await customerCartItemLinkRepo.find();

expect(cartItem).to.deepEqual(persistedItem);
expect(persistedLink).have.length(1);
const expected = {
customerId: existingCustomerId,
itemId: cartItem.id,
};
expect(toJSON(persistedLink[0])).to.containEql(toJSON(expected));
});

it('finds an instance via through model', async () => {
const item = await customerCartItemRepo.create({
description: 'an item hasManyThrough',
});
const notMyItem = await cartItemRepo.create({
description: "someone else's item desc",
});

const items = await customerCartItemRepo.find();

expect(items).to.not.containEql(notMyItem);
expect(items).to.deepEqual([item]);
});

it('finds instances via through models', async () => {
const item1 = await customerCartItemRepo.create({description: 'group 1'});
const item2 = await customerCartItemRepo.create({
description: 'group 2',
});
const items = await customerCartItemRepo.find();

expect(items).have.length(2);
expect(items).to.deepEqual([item1, item2]);
const group1 = await customerCartItemRepo.find({
where: {description: 'group 1'},
});
expect(group1).to.deepEqual([item1]);
});

it('deletes an instance, then deletes the through model', async () => {
await customerCartItemRepo.create({
description: 'customer 1',
});
const anotherHasManyThroughRepo = customerCartItemFactory(
existingCustomerId + 1,
);
const item2 = await anotherHasManyThroughRepo.create({
description: 'customer 2',
});
let items = await cartItemRepo.find();
let links = await customerCartItemLinkRepo.find();

expect(items).have.length(2);
expect(links).have.length(2);

await customerCartItemRepo.delete();
items = await cartItemRepo.find();
links = await customerCartItemLinkRepo.find();

expect(items).have.length(1);
expect(links).have.length(1);
expect(items).to.deepEqual([item2]);
expect(links[0]).has.property('itemId', item2.id);
expect(links[0]).has.property('customerId', existingCustomerId + 1);
});

it('deletes through model when corresponding target gets deleted', async () => {
const item1 = await customerCartItemRepo.create({
description: 'customer 1',
});
const anotherHasManyThroughRepo = customerCartItemFactory(
existingCustomerId + 1,
);
const item2 = await anotherHasManyThroughRepo.create({
description: 'customer 2',
});
// when order1 gets deleted, this through instance should be deleted too.
const through = await customerCartItemLinkRepo.create({
id: 1,
customerId: existingCustomerId + 1,
itemId: item1.id,
});
let items = await cartItemRepo.find();
let links = await customerCartItemLinkRepo.find();

expect(items).have.length(2);
expect(links).have.length(3);

await customerCartItemRepo.delete();

items = await cartItemRepo.find();
links = await customerCartItemLinkRepo.find();

expect(items).have.length(1);
expect(links).have.length(1);
expect(items).to.deepEqual([item2]);
expect(links).to.not.containEql(through);
expect(links[0]).has.property('itemId', item2.id);
expect(links[0]).has.property('customerId', existingCustomerId + 1);
});

it('deletes instances based on the filter', async () => {
await customerCartItemRepo.create({
description: 'customer 1',
});
const item2 = await customerCartItemRepo.create({
description: 'customer 2',
});

let items = await cartItemRepo.find();
let links = await customerCartItemLinkRepo.find();
expect(items).have.length(2);
expect(links).have.length(2);

await customerCartItemRepo.delete({description: 'does not exist'});
items = await cartItemRepo.find();
links = await customerCartItemLinkRepo.find();
expect(items).have.length(2);
expect(links).have.length(2);

await customerCartItemRepo.delete({description: 'customer 1'});
items = await cartItemRepo.find();
links = await customerCartItemLinkRepo.find();

expect(items).have.length(1);
expect(links).have.length(1);
expect(items).to.deepEqual([item2]);
expect(links[0]).has.property('itemId', item2.id);
expect(links[0]).has.property('customerId', existingCustomerId);
});

it('patches instances that belong to the same source model (same source fk)', async () => {
const item1 = await customerCartItemRepo.create({
description: 'group 1',
});
const item2 = await customerCartItemRepo.create({
description: 'group 1',
});

const count = await customerCartItemRepo.patch({description: 'group 2'});
expect(count).to.match({count: 2});
const updateResult = await cartItemRepo.find();
expect(toJSON(updateResult)).to.containDeep(
toJSON([
{id: item1.id, description: 'group 2'},
{id: item2.id, description: 'group 2'},
]),
);
});

it('links a target instance to the source instance', async () => {
const item = await cartItemRepo.create({description: 'an item'});
let targets = await customerCartItemRepo.find();
expect(targets).to.deepEqual([]);

await customerCartItemRepo.link(item.id);
targets = await customerCartItemRepo.find();
expect(toJSON(targets)).to.containDeep(toJSON([item]));
const link = await customerCartItemLinkRepo.find();
expect(toJSON(link[0])).to.containEql(
toJSON({customerId: existingCustomerId, itemId: item.id}),
);
});

it('unlinks a target instance from the source instance', async () => {
const item = await customerCartItemRepo.create({description: 'an item'});
let targets = await customerCartItemRepo.find();
expect(toJSON(targets)).to.containDeep(toJSON([item]));

await customerCartItemRepo.unlink(item.id);
targets = await customerCartItemRepo.find();
expect(targets).to.deepEqual([]);
// the through model should be deleted
const thoughs = await customerCartItemRepo.find();
expect(thoughs).to.deepEqual([]);
});
//--- HELPERS ---//

async function givenPersistedCustomerInstance() {
const customer = await customerRepo.create({name: 'a customer'});
existingCustomerId = customer.id;
}

function givenConstrainedRepositories() {
customerCartItemFactory = createHasAndBelongsToManyRepositoryFactory<
CustomerCartItemLink,
CartItem,
typeof Customer.prototype.id,
typeof CustomerCartItemLink.prototype.id,
typeof CartItem.prototype.id
>(
{
name: 'cartItems',
type: 'hasAndBelongsToMany',
targetsMany: true,
source: Customer,
keyFrom: 'id',
target: () => CartItem,
keyTo: 'id',
through: {
model: () => CustomerCartItemLink,
sourceKey: 'customerId',
targetKey: 'itemId',
},
} as HasAndBelongsToManyDefinition,
Getter.fromValue(customerCartItemLinkRepo),
Getter.fromValue(cartItemRepo),
);

customerCartItemRepo = customerCartItemFactory(existingCustomerId);
}
});

//--- HELPERS ---//

class Order extends Entity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
belongsTo,
Entity,
getModelRelations,
hasAndBelongsToMany,
hasMany,
model,
MODEL_PROPERTIES_KEY,
Expand All @@ -18,6 +19,66 @@ import {
} from '../../..';

describe('relation decorator', () => {
describe('hasAndBelongsToMany', () => {
it('takes in complex property type and infers foreign key via source model name', () => {
// Source Model
@model()
class Rol extends Entity {
id: number;
name: string;
description?: string;
@hasAndBelongsToMany(() => RolesHasPermissions, () => Permission)
permissions: object;
}

// Target Model
@model()
class Permission extends Entity {
id: number;
name: string;
description?: string;
resource: string;
scope: string;
}

// Through Model
@model()
class RolesHasPermissions extends Entity {
rolId: number;
permissionId: number;
}

const meta = MetadataInspector.getPropertyMetadata(
RELATIONS_KEY,
Rol.prototype,
'permissions',
);

expect(meta).to.eql({
name: 'permissions',
type: RelationType.hasAndBelongsToMany,
targetsMany: true,
source: Rol,
target: () => Permission,
through: {
model: () => RolesHasPermissions,
},
});
expect(Rol.definition.relations).to.eql({
permissions: {
name: 'permissions',
type: RelationType.hasAndBelongsToMany,
targetsMany: true,
source: Rol,
target: () => Permission,
through: {
model: () => RolesHasPermissions,
},
},
});
});
});

describe('hasMany', () => {
it('takes in complex property type and infers foreign key via source model name', () => {
@model()
Expand Down
Loading