Skip to content

Commit

Permalink
feat: source category entity (#2119)
Browse files Browse the repository at this point in the history
- Introduce source category entity.
- Add `categoryId` column on the source entity.
- update sources query to filter by category

MI-483
  • Loading branch information
sshanzel authored Aug 14, 2024
1 parent f324fc8 commit 362d6ef
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 10 deletions.
1 change: 1 addition & 0 deletions __tests__/__snapshots__/sourceRequests.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Object {
exports[`mutation publishSourceRequest should publish a source request 2`] = `
MachineSource {
"active": true,
"categoryId": null,
"color": null,
"description": null,
"flags": Object {},
Expand Down
137 changes: 127 additions & 10 deletions __tests__/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { DisallowHandle } from '../src/entity/DisallowHandle';
import { NotificationType } from '../src/notifications/common';
import { SourceTagView } from '../src/entity/SourceTagView';
import { isNullOrUndefined } from '../src/common/object';
import { SourceCategory } from '../src/entity/sources/SourceCategory';

let con: DataSource;
let state: GraphQLTestingState;
Expand All @@ -55,9 +56,53 @@ beforeAll(async () => {
client = state.client;
});

const getSourceCategories = () => [
{
title: 'General',
enabled: true,
},
{
title: 'Web',
enabled: true,
},
{
title: 'Mobile',
enabled: true,
},
{
title: 'Games',
enabled: true,
},
{
title: 'DevOps',
enabled: true,
},
{
title: 'Cloud',
enabled: true,
},
{
title: 'Career',
enabled: true,
},
{
title: 'Data',
enabled: true,
},
{
title: 'Fun',
enabled: true,
},
{
title: 'DevTools',
enabled: true,
},
];

beforeEach(async () => {
loggedUser = null;
premiumUser = false;
await saveFixtures(con, SourceCategory, getSourceCategories());
await saveFixtures(con, Source, [
sourcesFixture[0],
sourcesFixture[1],
Expand Down Expand Up @@ -105,16 +150,52 @@ beforeEach(async () => {

afterAll(() => disposeGraphQLTesting(state));

describe('query sourceCategories', () => {
it('should return source categories', async () => {
const res = await client.query(`
query SourceCategories($first: Int, $after: String) {
sourceCategories(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
title
}
}
}
}
`);
expect(res.errors).toBeFalsy();
const categories = getSourceCategories();
const isAllFound = res.data.sourceCategories.edges.every(({ node }) =>
categories.some((category) => category.title === node.title),
);
expect(isAllFound).toBeTruthy();
});
});

describe('query sources', () => {
const QUERY = (
interface Props {
first: number;
featured: boolean;
filterOpenSquads: boolean;
categoryId: string;
}

const QUERY = ({
first = 10,
filterOpenSquads = false,
featured?: boolean,
): string => `{
featured,
categoryId,
}: Partial<Props> = {}): string => `{
sources(
first: ${first},
filterOpenSquads: ${filterOpenSquads}
${isNullOrUndefined(featured) ? '' : `, featured: ${featured}`}
${isNullOrUndefined(categoryId) ? '' : `, categoryId: "${categoryId}"`}
) {
pageInfo {
endCursor
Expand All @@ -132,17 +213,39 @@ describe('query sources', () => {
flags {
featured
}
category {
id
}
}
}
}
}`;

it('should return only public sources', async () => {
const res = await client.query(QUERY(10, true));
const res = await client.query(
QUERY({ first: 10, filterOpenSquads: true }),
);
const isPublic = res.data.sources.edges.every(({ node }) => !!node.public);
expect(isPublic).toBeTruthy();
});

it('should filter by category', async () => {
const repo = con.getRepository(Source);
const general = await con
.getRepository(SourceCategory)
.findOneByOrFail({ title: 'General' });
const web = await con
.getRepository(SourceCategory)
.findOneByOrFail({ title: 'Web' });
await repo.update({ id: 'a' }, { categoryId: general.id });
await repo.update({ id: 'b' }, { categoryId: web.id });
const res = await client.query(QUERY({ first: 10, categoryId: web.id }));
const isAllWeb = res.data.sources.edges.every(
({ node }) => node.category.id === web.id,
);
expect(isAllWeb).toBeTruthy();
});

const prepareFeaturedTests = async () => {
const repo = con.getRepository(Source);
await repo.update(
Expand All @@ -157,7 +260,9 @@ describe('query sources', () => {

it('should return only featured sources', async () => {
await prepareFeaturedTests();
const res = await client.query(QUERY(10, false, true));
const res = await client.query(
QUERY({ first: 10, filterOpenSquads: false, featured: true }),
);
const isFeatured = res.data.sources.edges.every(
({ node }) => !!node.flags.featured,
);
Expand All @@ -166,15 +271,21 @@ describe('query sources', () => {

it('should return only not featured sources', async () => {
await prepareFeaturedTests();
const res = await client.query(QUERY(10, false, false));
const res = await client.query(
QUERY({
first: 10,
filterOpenSquads: false,
featured: false,
}),
);
const isNotFeatured = res.data.sources.edges.every(
({ node }) => !node.flags.featured,
);
expect(isNotFeatured).toBeTruthy();
});

it('should flag that more pages available', async () => {
const res = await client.query(QUERY(1));
const res = await client.query(QUERY({ first: 1 }));
expect(res.data.sources.pageInfo.hasNextPage).toBeTruthy();
});

Expand All @@ -197,7 +308,9 @@ describe('query sources', () => {

const prepareSquads = async () => {
const repo = con.getRepository(Source);
const res = await client.query(QUERY(10, true));
const res = await client.query(
QUERY({ first: 10, filterOpenSquads: true }),
);
expect(res.errors).toBeFalsy();
expect(res.data.sources.edges.length).toEqual(0);

Expand All @@ -211,7 +324,9 @@ describe('query sources', () => {
it('should return only public squads', async () => {
await prepareSquads();

const res = await client.query(QUERY(10, true));
const res = await client.query(
QUERY({ first: 10, filterOpenSquads: true }),
);
expect(res.errors).toBeFalsy();
expect(res.data.sources.edges.length).toEqual(1);
const allSquad = res.data.sources.edges.every(
Expand All @@ -222,7 +337,9 @@ describe('query sources', () => {

it('should return public squad color and headerImage', async () => {
await prepareSquads();
const res = await client.query(QUERY(10, true));
const res = await client.query(
QUERY({ first: 10, filterOpenSquads: true }),
);
expect(res.errors).toBeFalsy();
expect(res.data.sources.edges.length).toEqual(1);
expect(res.data.sources.edges[0].node.public).toBeTruthy();
Expand Down
11 changes: 11 additions & 0 deletions src/entity/Source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Column,
Entity,
Index,
ManyToOne,
OneToMany,
PrimaryColumn,
TableInheritance,
Expand All @@ -11,6 +12,7 @@ import { SourceDisplay } from './SourceDisplay';
import { SourceFeed } from './SourceFeed';
import { Post } from './posts';
import { SourceMember } from './SourceMember';
import { SourceCategory } from './sources/SourceCategory';

export const COMMUNITY_PICKS_SOURCE = 'community';

Expand Down Expand Up @@ -109,6 +111,15 @@ export class Source {
@Index('IDX_source_flags_featured', { synchronize: false })
flags: SourceFlagsPublic;

@Column({ type: 'text', nullable: true })
categoryId?: string;

@ManyToOne(() => SourceCategory, (category) => category.id, {
lazy: true,
onDelete: 'SET NULL',
})
category: Promise<SourceCategory>;

@OneToMany(() => SourceDisplay, (display) => display.source, { lazy: true })
displays: Promise<SourceDisplay[]>;

Expand Down
25 changes: 25 additions & 0 deletions src/entity/sources/SourceCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

@Entity()
export class SourceCategory {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'text', unique: true })
title: string;

@Column()
enabled: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
3 changes: 3 additions & 0 deletions src/graphorm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ const obj = new GraphORM({
},
},
},
SourceCategory: {
requiredColumns: ['createdAt'],
},
Source: {
requiredColumns: ['id', 'private', 'handle', 'type'],
fields: {
Expand Down
40 changes: 40 additions & 0 deletions src/migration/1723579275223-SourceCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class SourceCategory1723579275223 implements MigrationInterface {
name = 'SourceCategory1723579275223';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "source_category" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" text NOT NULL, "enabled" boolean NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_66d6dccf282b8104ef9c44c0fb5" UNIQUE ("title"), CONSTRAINT "PK_21e4d5359f2a23fd10053f516e9" PRIMARY KEY ("id"))`,
);
await queryRunner.query(`ALTER TABLE "source" ADD "categoryId" uuid`);
await queryRunner.query(
`ALTER TABLE "source" ADD CONSTRAINT "FK_02e1cbb6e33fa90e68dd56de2a9" FOREIGN KEY ("categoryId") REFERENCES "source_category"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(`
INSERT INTO "source_category"
(title, enabled)
VALUES
('General', true),
('Web', true),
('Mobile', true),
('Games', true),
('DevOps', true),
('Cloud', true),
('Career', true),
('Data', true),
('Fun', true),
('DevTools', true)
ON CONFLICT DO NOTHING;
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`TRUNCATE "source_category"`);
await queryRunner.query(
`ALTER TABLE "source" DROP CONSTRAINT "FK_02e1cbb6e33fa90e68dd56de2a9"`,
);
await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "categoryId"`);
await queryRunner.query(`DROP TABLE "source_category"`);
}
}
Loading

0 comments on commit 362d6ef

Please sign in to comment.