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

feat: source category entity #2119

Merged
merged 13 commits into from
Aug 14, 2024
65 changes: 65 additions & 0 deletions __tests__/__snapshots__/sources.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,71 @@ Object {
}
`;

exports[`query sourceCategories should return source categories 1`] = `
Array [
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "general",
"value": "General",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "web",
"value": "Web",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "mobile",
"value": "Mobile",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "games",
"value": "Games",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "devops",
"value": "DevOps",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "cloud",
"value": "Cloud",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "career",
"value": "Career",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "data",
"value": "Data",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "fun",
"value": "Fun",
},
Object {
"createdAt": Any<String>,
"enabled": true,
"id": "devtools",
"value": "DevTools",
},
]
`;

exports[`query sourceMembers should return blocked users only when user is a moderator 1`] = `
Object {
"sourceMembers": Object {
Expand Down
132 changes: 122 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,34 @@ 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 () => {
console.log('entry');
const repo = con.getRepository(Source);
await repo.update({ id: 'a' }, { categoryId: 'general' });
await repo.update({ id: 'b' }, { categoryId: 'web' });
const res = await client.query(QUERY({ first: 10, categoryId: 'web' }));
rebelchris marked this conversation as resolved.
Show resolved Hide resolved
const isAllWeb = res.data.sources.edges.every(
({ node }) => node.category.id === 'web',
);
expect(isAllWeb).toBeTruthy();
});

const prepareFeaturedTests = async () => {
const repo = con.getRepository(Source);
await repo.update(
Expand All @@ -157,7 +255,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 +266,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 +303,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 +319,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 +332,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
8 changes: 8 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,12 @@ 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 })
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 {
sshanzel marked this conversation as resolved.
Show resolved Hide resolved
@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'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this one required only?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its due to paginated query being ordered by date so it needs to be included in graphorm context always

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createdAt is needed for the pagination, technically you can have everything missing and not break the query.

},
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 NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(`
INSERT INTO "source_category"
rebelchris marked this conversation as resolved.
Show resolved Hide resolved
(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