@@ -209,6 +209,13 @@ class RequestHandler extends APIHandlerBase {
209
209
data : z . array ( z . object ( { type : z . string ( ) , id : z . union ( [ z . string ( ) , z . number ( ) ] ) } ) ) ,
210
210
} ) ;
211
211
212
+ private upsertMetaSchema = z . object ( {
213
+ meta : z . object ( {
214
+ operation : z . literal ( 'upsert' ) ,
215
+ matchFields : z . array ( z . string ( ) ) . min ( 1 ) ,
216
+ } ) ,
217
+ } ) ;
218
+
212
219
// all known types and their metadata
213
220
private typeMap : Record < string , ModelInfo > ;
214
221
@@ -309,8 +316,29 @@ class RequestHandler extends APIHandlerBase {
309
316
310
317
let match = this . urlPatterns . collection . match ( path ) ;
311
318
if ( match ) {
312
- // resource creation
313
- return await this . processCreate ( prisma , match . type , query , requestBody , modelMeta , zodSchemas ) ;
319
+ const body = requestBody as any ;
320
+ const upsertMeta = this . upsertMetaSchema . safeParse ( body ) ;
321
+ if ( upsertMeta . success ) {
322
+ // resource upsert
323
+ return await this . processUpsert (
324
+ prisma ,
325
+ match . type ,
326
+ query ,
327
+ requestBody ,
328
+ modelMeta ,
329
+ zodSchemas
330
+ ) ;
331
+ } else {
332
+ // resource creation
333
+ return await this . processCreate (
334
+ prisma ,
335
+ match . type ,
336
+ query ,
337
+ requestBody ,
338
+ modelMeta ,
339
+ zodSchemas
340
+ ) ;
341
+ }
314
342
}
315
343
316
344
match = this . urlPatterns . relationship . match ( path ) ;
@@ -809,6 +837,90 @@ class RequestHandler extends APIHandlerBase {
809
837
} ;
810
838
}
811
839
840
+ private async processUpsert (
841
+ prisma : DbClientContract ,
842
+ type : string ,
843
+ _query : Record < string , string | string [ ] > | undefined ,
844
+ requestBody : unknown ,
845
+ modelMeta : ModelMeta ,
846
+ zodSchemas ?: ZodSchemas
847
+ ) {
848
+ const typeInfo = this . typeMap [ type ] ;
849
+ if ( ! typeInfo ) {
850
+ return this . makeUnsupportedModelError ( type ) ;
851
+ }
852
+
853
+ const { error, attributes, relationships } = this . processRequestBody ( type , requestBody , zodSchemas , 'create' ) ;
854
+
855
+ if ( error ) {
856
+ return error ;
857
+ }
858
+
859
+ const matchFields = this . upsertMetaSchema . parse ( requestBody ) . meta . matchFields ;
860
+
861
+ const uniqueFields = Object . values ( modelMeta . models [ type ] . uniqueConstraints || { } ) . map ( ( uf ) => uf . fields ) ;
862
+
863
+ if (
864
+ ! uniqueFields . some ( ( uniqueCombination ) => uniqueCombination . every ( ( field ) => matchFields . includes ( field ) ) )
865
+ ) {
866
+ return this . makeError ( 'invalidPayload' , 'Match fields must be unique fields' , 400 ) ;
867
+ }
868
+
869
+ const upsertPayload : any = {
870
+ where : this . makeUpsertWhere ( matchFields , attributes , typeInfo ) ,
871
+ create : { ...attributes } ,
872
+ update : {
873
+ ...Object . fromEntries ( Object . entries ( attributes ) . filter ( ( e ) => ! matchFields . includes ( e [ 0 ] ) ) ) ,
874
+ } ,
875
+ } ;
876
+
877
+ if ( relationships ) {
878
+ for ( const [ key , data ] of Object . entries < any > ( relationships ) ) {
879
+ if ( ! data ?. data ) {
880
+ return this . makeError ( 'invalidRelationData' ) ;
881
+ }
882
+
883
+ const relationInfo = typeInfo . relationships [ key ] ;
884
+ if ( ! relationInfo ) {
885
+ return this . makeUnsupportedRelationshipError ( type , key , 400 ) ;
886
+ }
887
+
888
+ if ( relationInfo . isCollection ) {
889
+ upsertPayload . create [ key ] = {
890
+ connect : enumerate ( data . data ) . map ( ( item : any ) =>
891
+ this . makeIdConnect ( relationInfo . idFields , item . id )
892
+ ) ,
893
+ } ;
894
+ upsertPayload . update [ key ] = {
895
+ set : enumerate ( data . data ) . map ( ( item : any ) =>
896
+ this . makeIdConnect ( relationInfo . idFields , item . id )
897
+ ) ,
898
+ } ;
899
+ } else {
900
+ if ( typeof data . data !== 'object' ) {
901
+ return this . makeError ( 'invalidRelationData' ) ;
902
+ }
903
+ upsertPayload . create [ key ] = {
904
+ connect : this . makeIdConnect ( relationInfo . idFields , data . data . id ) ,
905
+ } ;
906
+ upsertPayload . update [ key ] = {
907
+ connect : this . makeIdConnect ( relationInfo . idFields , data . data . id ) ,
908
+ } ;
909
+ }
910
+ }
911
+ }
912
+
913
+ // include IDs of relation fields so that they can be serialized.
914
+ this . includeRelationshipIds ( type , upsertPayload , 'include' ) ;
915
+
916
+ const entity = await prisma [ type ] . upsert ( upsertPayload ) ;
917
+
918
+ return {
919
+ status : 201 ,
920
+ body : await this . serializeItems ( type , entity ) ,
921
+ } ;
922
+ }
923
+
812
924
private async processRelationshipCRUD (
813
925
prisma : DbClientContract ,
814
926
mode : 'create' | 'update' | 'delete' ,
@@ -1296,6 +1408,24 @@ class RequestHandler extends APIHandlerBase {
1296
1408
return idFields . map ( ( idf ) => item [ idf . name ] ) . join ( this . idDivider ) ;
1297
1409
}
1298
1410
1411
+ private makeUpsertWhere ( matchFields : any [ ] , attributes : any , typeInfo : ModelInfo ) {
1412
+ const where = matchFields . reduce ( ( acc : any , field : string ) => {
1413
+ acc [ field ] = attributes [ field ] ?? null ;
1414
+ return acc ;
1415
+ } , { } ) ;
1416
+
1417
+ if (
1418
+ typeInfo . idFields . length > 1 &&
1419
+ matchFields . some ( ( mf ) => typeInfo . idFields . map ( ( idf ) => idf . name ) . includes ( mf ) )
1420
+ ) {
1421
+ return {
1422
+ [ this . makePrismaIdKey ( typeInfo . idFields ) ] : where ,
1423
+ } ;
1424
+ }
1425
+
1426
+ return where ;
1427
+ }
1428
+
1299
1429
private includeRelationshipIds ( model : string , args : any , mode : 'select' | 'include' ) {
1300
1430
const typeInfo = this . typeMap [ model ] ;
1301
1431
if ( ! typeInfo ) {
0 commit comments