Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0a7f703
chore: update sources to discriminated unions across the app
knudtty Mar 11, 2026
4724aa2
fix: useSource generic based on kind; fix lint issues
knudtty Mar 12, 2026
a696894
fix: make defaultSelectStatementExpression required on trace
knudtty Mar 12, 2026
2099581
Merge remote-tracking branch 'origin/main' into aaron/sources-but-better
knudtty Mar 12, 2026
4db2bc8
add changeset
knudtty Mar 12, 2026
4aaada9
fix linting
knudtty Mar 12, 2026
e676fac
PR suggestions
knudtty Mar 12, 2026
2aa4487
Merge remote-tracking branch 'origin/main' into aaron/sources-but-better
knudtty Mar 13, 2026
f003e05
fix: lint fixes
knudtty Mar 13, 2026
d09db9e
Merge remote-tracking branch 'origin/main' into aaron/sources-but-better
knudtty Mar 17, 2026
fe8e4bc
prevent breaking changes by applying legacy default timestamp to sources
knudtty Mar 17, 2026
0bfd364
feat: display toast to user if source doesn't match properly
knudtty Mar 17, 2026
402547a
Merge remote-tracking branch 'origin/main' into aaron/sources-but-better
knudtty Mar 17, 2026
5ca7eae
fix k8s dashboard
knudtty Mar 18, 2026
1fa7edd
Merge branch 'main' into aaron/sources-but-better
knudtty Mar 18, 2026
5e6fb9d
pr suggestion updates
knudtty Mar 18, 2026
6c7650a
Merge remote-tracking branch 'origin/main' into aaron/sources-but-better
knudtty Mar 18, 2026
d73d46d
remove session source warning
knudtty Mar 18, 2026
71ca3c0
fix rawsqlcharteditor oopsie
knudtty Mar 18, 2026
5763acd
Merge branch 'main' into aaron/sources-but-better
knudtty Mar 18, 2026
a48ca67
Merge branch 'main' into aaron/sources-but-better
knudtty Mar 19, 2026
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
7 changes: 7 additions & 0 deletions .changeset/six-ways-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---

fix: change sources to discriminated union
97 changes: 72 additions & 25 deletions packages/api/src/controllers/sources.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { SourceKind } from '@hyperdx/common-utils/dist/types';

import { ISource, Source } from '@/models/source';

/**
* Clean up metricTables property when changing source type away from Metric.
* This prevents metric-specific configuration from persisting when switching
* to Log, Trace, or Session sources.
*/
function cleanSourceData(source: Omit<ISource, 'id'>): Omit<ISource, 'id'> {
// Only clean metricTables if the source is not a Metric type
if (source.kind !== SourceKind.Metric) {
// explicitly setting to null for mongoose to clear column
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
source.metricTables = null as any;
}
import { SourceKind, SourceSchema } from '@hyperdx/common-utils/dist/types';

import {
ISourceInput,
LogSource,
MetricSource,
SessionSource,
Source,
TraceSource,
} from '@/models/source';

return source;
// Returns the discriminator model for the given source kind.
// Updates must go through the correct discriminator model so Mongoose
// recognises kind-specific fields (e.g. metricTables on MetricSource).
function getModelForKind(kind: SourceKind) {
switch (kind) {
case SourceKind.Log:
return LogSource;
case SourceKind.Trace:
return TraceSource;
case SourceKind.Session:
return SessionSource;
case SourceKind.Metric:
return MetricSource;
default:
kind satisfies never;
throw new Error(`${kind} is not a valid SourceKind`);
}
}

export function getSources(team: string) {
Expand All @@ -26,19 +36,56 @@ export function getSource(team: string, sourceId: string) {
return Source.findOne({ _id: sourceId, team });
}

export function createSource(team: string, source: Omit<ISource, 'id'>) {
return Source.create({ ...source, team });
type DistributiveOmit<T, K extends PropertyKey> = T extends T
? Omit<T, K>
: never;

export function createSource(
team: string,
source: DistributiveOmit<ISourceInput, 'id'>,
) {
// @ts-expect-error The create method has incompatible type signatures but is actually safe
return getModelForKind(source.kind)?.create({ ...source, team });
}
Comment thread
pulpdrew marked this conversation as resolved.

export function updateSource(
export async function updateSource(
team: string,
sourceId: string,
source: Omit<ISource, 'id'>,
source: DistributiveOmit<ISourceInput, 'id'>,
) {
const cleanedSource = cleanSourceData(source);
return Source.findOneAndUpdate({ _id: sourceId, team }, cleanedSource, {
new: true,
});
const existing = await Source.findOne({ _id: sourceId, team });
if (!existing) return null;

// Same kind: simple update through the discriminator model
if (existing.kind === source.kind) {
// @ts-expect-error The findOneAndUpdate method has incompatible type signatures but is actually safe
return getModelForKind(source.kind)?.findOneAndUpdate(
{ _id: sourceId, team },
source,
Comment thread Dismissed
{ new: true },
);
}

// Kind changed: validate through Zod before writing since the raw
// collection bypass skips Mongoose's discriminator validation.
const parseResult = SourceSchema.safeParse(source);
if (!parseResult.success) {
throw new Error(
`Invalid source data: ${parseResult.error.errors.map(e => e.message).join(', ')}`,
);
}

// Use replaceOne on the raw collection to swap the entire document
// in place (including the discriminator key). This is a single atomic
// write — the document is never absent from the collection.
const replacement = {
...parseResult.data,
_id: existing._id,
team: existing.team,
updatedAt: new Date(),
};
await Source.collection.replaceOne({ _id: existing._id }, replacement);
return getModelForKind(replacement.kind)?.hydrate(replacement);
}

export function deleteSource(team: string, sourceId: string) {
Expand Down
Loading
Loading