1- import { SourceKind } from '@hyperdx/common-utils/dist/types' ;
2-
3- import { ISource , Source } from '@/models/source' ;
4-
5- /**
6- * Clean up metricTables property when changing source type away from Metric.
7- * This prevents metric-specific configuration from persisting when switching
8- * to Log, Trace, or Session sources.
9- */
10- function cleanSourceData ( source : Omit < ISource , 'id' > ) : Omit < ISource , 'id' > {
11- // Only clean metricTables if the source is not a Metric type
12- if ( source . kind !== SourceKind . Metric ) {
13- // explicitly setting to null for mongoose to clear column
14- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
15- source . metricTables = null as any ;
16- }
1+ import { SourceKind , SourceSchema } from '@hyperdx/common-utils/dist/types' ;
2+
3+ import {
4+ ISourceInput ,
5+ LogSource ,
6+ MetricSource ,
7+ SessionSource ,
8+ Source ,
9+ TraceSource ,
10+ } from '@/models/source' ;
1711
18- return source ;
12+ // Returns the discriminator model for the given source kind.
13+ // Updates must go through the correct discriminator model so Mongoose
14+ // recognises kind-specific fields (e.g. metricTables on MetricSource).
15+ function getModelForKind ( kind : SourceKind ) {
16+ switch ( kind ) {
17+ case SourceKind . Log :
18+ return LogSource ;
19+ case SourceKind . Trace :
20+ return TraceSource ;
21+ case SourceKind . Session :
22+ return SessionSource ;
23+ case SourceKind . Metric :
24+ return MetricSource ;
25+ default :
26+ kind satisfies never ;
27+ throw new Error ( `${ kind } is not a valid SourceKind` ) ;
28+ }
1929}
2030
2131export function getSources ( team : string ) {
@@ -26,19 +36,56 @@ export function getSource(team: string, sourceId: string) {
2636 return Source . findOne ( { _id : sourceId , team } ) ;
2737}
2838
29- export function createSource ( team : string , source : Omit < ISource , 'id' > ) {
30- return Source . create ( { ...source , team } ) ;
39+ type DistributiveOmit < T , K extends PropertyKey > = T extends T
40+ ? Omit < T , K >
41+ : never ;
42+
43+ export function createSource (
44+ team : string ,
45+ source : DistributiveOmit < ISourceInput , 'id' > ,
46+ ) {
47+ // @ts -expect-error The create method has incompatible type signatures but is actually safe
48+ return getModelForKind ( source . kind ) ?. create ( { ...source , team } ) ;
3149}
3250
33- export function updateSource (
51+ export async function updateSource (
3452 team : string ,
3553 sourceId : string ,
36- source : Omit < ISource , 'id' > ,
54+ source : DistributiveOmit < ISourceInput , 'id' > ,
3755) {
38- const cleanedSource = cleanSourceData ( source ) ;
39- return Source . findOneAndUpdate ( { _id : sourceId , team } , cleanedSource , {
40- new : true ,
41- } ) ;
56+ const existing = await Source . findOne ( { _id : sourceId , team } ) ;
57+ if ( ! existing ) return null ;
58+
59+ // Same kind: simple update through the discriminator model
60+ if ( existing . kind === source . kind ) {
61+ // @ts -expect-error The findOneAndUpdate method has incompatible type signatures but is actually safe
62+ return getModelForKind ( source . kind ) ?. findOneAndUpdate (
63+ { _id : sourceId , team } ,
64+ source ,
65+ { new : true } ,
66+ ) ;
67+ }
68+
69+ // Kind changed: validate through Zod before writing since the raw
70+ // collection bypass skips Mongoose's discriminator validation.
71+ const parseResult = SourceSchema . safeParse ( source ) ;
72+ if ( ! parseResult . success ) {
73+ throw new Error (
74+ `Invalid source data: ${ parseResult . error . errors . map ( e => e . message ) . join ( ', ' ) } ` ,
75+ ) ;
76+ }
77+
78+ // Use replaceOne on the raw collection to swap the entire document
79+ // in place (including the discriminator key). This is a single atomic
80+ // write — the document is never absent from the collection.
81+ const replacement = {
82+ ...parseResult . data ,
83+ _id : existing . _id ,
84+ team : existing . team ,
85+ updatedAt : new Date ( ) ,
86+ } ;
87+ await Source . collection . replaceOne ( { _id : existing . _id } , replacement ) ;
88+ return getModelForKind ( replacement . kind ) ?. hydrate ( replacement ) ;
4289}
4390
4491export function deleteSource ( team : string , sourceId : string ) {
0 commit comments