mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: query settings length validation (#1890)
## Summary Fixes an issue with query settings length validation. `maxlength` only works for strings, so the max amount of query settings (10) was never enforced by mongoose. Adds a small validator to address this. ### How to test locally or on Vercel The max length is already enforced in the app, so make a HTTP request directly to API - or trust the integration tests :)
This commit is contained in:
parent
8938b2741b
commit
e09c8c0e5d
4 changed files with 161 additions and 10 deletions
6
.changeset/honest-icons-fix.md
Normal file
6
.changeset/honest-icons-fix.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@hyperdx/api": patch
|
||||
"@hyperdx/common-utils": patch
|
||||
---
|
||||
|
||||
fix: query settings length validation
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
MetricsDataType,
|
||||
QuerySettings,
|
||||
SourceKind,
|
||||
TSource,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
|
|
@ -14,6 +15,27 @@ export interface ISource extends Omit<TSource, 'connection'> {
|
|||
|
||||
export type SourceDocument = mongoose.HydratedDocument<ISource>;
|
||||
|
||||
const maxLength =
|
||||
(max: number) =>
|
||||
<T>({ length }: Array<T>) =>
|
||||
length <= max;
|
||||
|
||||
const QuerySetting = new Schema<QuerySettings[number]>(
|
||||
{
|
||||
setting: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 1,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 1,
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
export const Source = mongoose.model<ISource>(
|
||||
'Source',
|
||||
new Schema<ISource>(
|
||||
|
|
@ -89,13 +111,11 @@ export const Source = mongoose.model<ISource>(
|
|||
},
|
||||
|
||||
querySettings: {
|
||||
type: [
|
||||
{
|
||||
setting: { type: String, required: true, minlength: 1 },
|
||||
value: { type: String, required: true, minlength: 1 },
|
||||
},
|
||||
],
|
||||
maxlength: 10,
|
||||
type: [QuerySetting],
|
||||
validate: {
|
||||
validator: maxLength(10),
|
||||
message: '{PATH} exceeds the limit of 10',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,6 +89,131 @@ describe('sources router', () => {
|
|||
.expect(400);
|
||||
});
|
||||
|
||||
describe('querySettings validation', () => {
|
||||
it('POST / - accepts and persists valid querySettings', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
const querySettings = [
|
||||
{ setting: 'max_execution_time', value: '60' },
|
||||
{ setting: 'max_memory_usage', value: '10000000000' },
|
||||
];
|
||||
|
||||
const response = await agent
|
||||
.post('/sources')
|
||||
.send({ ...MOCK_SOURCE, querySettings })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.querySettings).toEqual(querySettings);
|
||||
|
||||
const sources = await Source.find({}).lean();
|
||||
expect(sources).toHaveLength(1);
|
||||
expect(sources[0]?.querySettings).toEqual(querySettings);
|
||||
});
|
||||
|
||||
it('POST / - accepts querySettings at the limit of 10 items', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
const querySettings = Array.from({ length: 10 }, (_, i) => ({
|
||||
setting: `setting_${i}`,
|
||||
value: `value_${i}`,
|
||||
}));
|
||||
|
||||
const response = await agent
|
||||
.post('/sources')
|
||||
.send({ ...MOCK_SOURCE, querySettings })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.querySettings).toHaveLength(10);
|
||||
|
||||
const sources = await Source.find({}).lean();
|
||||
expect(sources[0]?.querySettings).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('POST / - rejects querySettings exceeding the limit of 10', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
const querySettings = Array.from({ length: 11 }, (_, i) => ({
|
||||
setting: `setting_${i}`,
|
||||
value: `value_${i}`,
|
||||
}));
|
||||
|
||||
const response = await agent
|
||||
.post('/sources')
|
||||
.send({ ...MOCK_SOURCE, querySettings });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const sources = await Source.find({}).lean();
|
||||
expect(sources).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('POST / - returns 400 when querySettings item has empty setting or value', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent
|
||||
.post('/sources')
|
||||
.send({
|
||||
...MOCK_SOURCE,
|
||||
querySettings: [{ setting: '', value: 'x' }],
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
await agent
|
||||
.post('/sources')
|
||||
.send({
|
||||
...MOCK_SOURCE,
|
||||
querySettings: [{ setting: 'x', value: '' }],
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('PUT /:id - accepts and persists valid querySettings', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
});
|
||||
|
||||
const querySettings = [{ setting: 'max_execution_time', value: '120' }];
|
||||
|
||||
await agent
|
||||
.put(`/sources/${source._id}`)
|
||||
.send({
|
||||
...MOCK_SOURCE,
|
||||
id: source._id.toString(),
|
||||
querySettings,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const updated = await Source.findById(source._id).lean();
|
||||
expect(updated?.querySettings).toEqual(querySettings);
|
||||
});
|
||||
|
||||
it('PUT /:id - rejects querySettings exceeding the limit of 10', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
});
|
||||
|
||||
const querySettings = Array.from({ length: 11 }, (_, i) => ({
|
||||
setting: `setting_${i}`,
|
||||
value: `value_${i}`,
|
||||
}));
|
||||
|
||||
const response = await agent.put(`/sources/${source._id}`).send({
|
||||
...MOCK_SOURCE,
|
||||
id: source._id.toString(),
|
||||
querySettings,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const updated = await Source.findById(source._id).lean();
|
||||
expect(updated?.querySettings).toEqual([]); // defaults to [] when source created
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT /:id - updates an existing source', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
|
|
|
|||
|
|
@ -785,9 +785,9 @@ export enum SourceKind {
|
|||
// TABLE SOURCE FORM VALIDATION
|
||||
// --------------------------
|
||||
|
||||
const QuerySettingsSchema = z.array(
|
||||
z.object({ setting: z.string().min(1), value: z.string().min(1) }),
|
||||
);
|
||||
const QuerySettingsSchema = z
|
||||
.array(z.object({ setting: z.string().min(1), value: z.string().min(1) }))
|
||||
.max(10);
|
||||
|
||||
export type QuerySettings = z.infer<typeof QuerySettingsSchema>;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue