Skip to content
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
30 changes: 30 additions & 0 deletions __tests__/routes/public/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import request from 'supertest';
import { setupPublicApiTests, createTokenForUser } from './helpers';
import { DatasetTool } from '../../../src/entity/dataset/DatasetTool';
import { UserStack } from '../../../src/entity/user/UserStack';
import { MAX_STACK_ITEMS } from '../../../src/common/constants';

const state = setupPublicApiTests();

Expand Down Expand Up @@ -107,6 +108,35 @@ describe('GET /public/v1/profile/stack', () => {

expect(body.data.length).toBeLessThanOrEqual(5);
});

it('should allow fetching up to maximum stack items', async () => {
const token = await createTokenForUser(state.con, '5');

const tools = await state.con.getRepository(DatasetTool).save(
Array.from({ length: 60 }, (_, index) => ({
title: `Public Stack Tool ${index}`,
titleNormalized: `publicstacktool${index}`,
faviconSource: 'none',
})),
);

await state.con.getRepository(UserStack).save(
tools.map((tool, index) => ({
userId: '5',
toolId: tool.id,
section: 'primary',
position: index,
})),
);

const { body } = await request(state.app.server)
.get('/public/v1/profile/stack')
.query({ limit: MAX_STACK_ITEMS })
.set('Authorization', `Bearer ${token}`)
.expect(200);

expect(body.data).toHaveLength(60);
});
});

describe('POST /public/v1/profile/stack', () => {
Expand Down
39 changes: 39 additions & 0 deletions __tests__/sourceStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DatasetTool } from '../src/entity/dataset/DatasetTool';
import { Source, SourceMember } from '../src/entity';
import { SourceMemberRoles } from '../src/roles';
import { sourcesFixture } from './fixture/source';
import { MAX_STACK_ITEMS } from '../src/common/constants';

let con: DataSource;
let state: GraphQLTestingState;
Expand Down Expand Up @@ -242,6 +243,44 @@ describe('mutation addSourceStack', () => {

expect(res.errors?.[0]?.message).toBe('Stack can only be added to Squads');
});

it('should prevent adding more than maximum stack items to squad', async () => {
loggedUser = '1';
await con.getRepository(SourceMember).save({
userId: '1',
sourceId: 'squad',
role: SourceMemberRoles.Admin,
referralToken: 'token1',
});

const tools = await con.getRepository(DatasetTool).save(
Array.from({ length: MAX_STACK_ITEMS }, (_, index) => ({
title: `Squad Tool ${index}`,
titleNormalized: `squadtool${index}`,
faviconSource: 'none',
})),
);

await con.getRepository(SourceStack).save(
tools.map((tool, index) => ({
sourceId: 'squad',
toolId: tool.id,
position: index,
createdById: '1',
})),
);

const res = await client.mutate(MUTATION, {
variables: {
sourceId: 'squad',
input: { title: 'Node.js' },
},
});

expect(res.errors?.[0]?.message).toBe(
`Squads can have a maximum of ${MAX_STACK_ITEMS} items in their stack`,
);
});
});

describe('mutation updateSourceStack', () => {
Expand Down
30 changes: 30 additions & 0 deletions __tests__/userStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { User } from '../src/entity/user/User';
import { usersFixture } from './fixture/user';
import { UserStack } from '../src/entity/user/UserStack';
import { DatasetTool } from '../src/entity/dataset/DatasetTool';
import { MAX_STACK_ITEMS } from '../src/common/constants';

let con: DataSource;
let state: GraphQLTestingState;
Expand Down Expand Up @@ -150,6 +151,35 @@ describe('mutation addUserStack', () => {
'Stack item already exists in your profile',
);
});

it('should prevent adding more than maximum stack items', async () => {
loggedUser = '1';

const tools = await con.getRepository(DatasetTool).save(
Array.from({ length: MAX_STACK_ITEMS }, (_, index) => ({
title: `Tool ${index}`,
titleNormalized: `tool${index}`,
faviconSource: 'none',
})),
);

await con.getRepository(UserStack).save(
tools.map((tool, index) => ({
userId: '1',
toolId: tool.id,
section: 'Runtime',
position: index,
})),
);

const res = await client.mutate(MUTATION, {
variables: { input: { title: 'Node.js', section: 'Runtime' } },
});

expect(res.errors?.[0]?.message).toBe(
`You can have a maximum of ${MAX_STACK_ITEMS} items in your stack`,
);
});
});

describe('mutation updateUserStack', () => {
Expand Down
1 change: 1 addition & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export const coresBalanceExpirationSeconds = 60 * ONE_MINUTE_IN_SECONDS;

// Position value for newly added items (tools, stack, hot takes)
export const NEW_ITEM_POSITION = 999999;
export const MAX_STACK_ITEMS = 100;

export const PUBLIC_API_PREFIX = '/public/v1';
10 changes: 7 additions & 3 deletions src/routes/public/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,15 @@ export interface FeedConnection<T> {
/**
* Parse and validate a limit query parameter.
* @param limitParam - The limit query string parameter
* @returns A valid limit between 1 and MAX_LIMIT
* @param maxLimit - The maximum limit allowed for this endpoint
* @returns A valid limit between 1 and maxLimit
*/
export const parseLimit = (limitParam?: string): number => {
export const parseLimit = (
limitParam?: string,
maxLimit: number = MAX_LIMIT,
): number => {
const parsed = parseInt(limitParam || '', 10) || DEFAULT_LIMIT;
return Math.min(Math.max(1, parsed), MAX_LIMIT);
return Math.min(Math.max(1, parsed), maxLimit);
};

/**
Expand Down
7 changes: 4 additions & 3 deletions src/routes/public/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
StackConnection,
ToolType,
} from './common';
import { MAX_STACK_ITEMS } from '../../common/constants';

// GraphQL queries
const AUTOCOMPLETE_TOOLS_QUERY = `
Expand Down Expand Up @@ -159,9 +160,9 @@ export default async function (fastify: FastifyInstance): Promise<void> {
limit: {
type: 'integer',
default: 20,
maximum: 50,
maximum: MAX_STACK_ITEMS,
minimum: 1,
description: 'Number of items to return (1-50)',
description: `Number of items to return (1-${MAX_STACK_ITEMS})`,
},
cursor: {
type: 'string',
Expand All @@ -183,7 +184,7 @@ export default async function (fastify: FastifyInstance): Promise<void> {
},
},
async (request, reply) => {
const limit = parseLimit(request.query.limit);
const limit = parseLimit(request.query.limit, MAX_STACK_ITEMS);
const { cursor } = request.query;
const con = ensureDbConnection(fastify.con);
const userId = request.userId;
Expand Down
11 changes: 10 additions & 1 deletion src/schema/sourceStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type ReorderSourceStackInput,
} from '../common/schema/sourceStack';
import { findOrCreateDatasetTool } from '../common/datasetTool';
import { NEW_ITEM_POSITION } from '../common/constants';
import { MAX_STACK_ITEMS, NEW_ITEM_POSITION } from '../common/constants';
import { ensureSourcePermissions, SourcePermissions } from './sources';
import { Source, SourceType } from '../entity/Source';

Expand Down Expand Up @@ -185,6 +185,15 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
throw new ValidationError('Stack item already exists in this Squad');
}

const count = await ctx.con.getRepository(SourceStack).count({
where: { sourceId: source.id },
});
if (count >= MAX_STACK_ITEMS) {
throw new ValidationError(
`Squads can have a maximum of ${MAX_STACK_ITEMS} items in their stack`,
);
}

const sourceStack = ctx.con.getRepository(SourceStack).create({
sourceId: source.id,
toolId: datasetTool.id,
Expand Down
11 changes: 10 additions & 1 deletion src/schema/userStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type ReorderUserStackInput,
} from '../common/schema/userStack';
import { findOrCreateDatasetTool } from '../common/datasetTool';
import { NEW_ITEM_POSITION } from '../common/constants';
import { MAX_STACK_ITEMS, NEW_ITEM_POSITION } from '../common/constants';

interface GQLUserStack {
id: string;
Expand Down Expand Up @@ -161,6 +161,15 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
throw new ValidationError('Stack item already exists in your profile');
}

const count = await ctx.con.getRepository(UserStack).count({
where: { userId: ctx.userId },
});
if (count >= MAX_STACK_ITEMS) {
throw new ValidationError(
`You can have a maximum of ${MAX_STACK_ITEMS} items in your stack`,
);
}

const userStack = ctx.con.getRepository(UserStack).create({
userId: ctx.userId,
toolId: datasetTool.id,
Expand Down
Loading