@@ -209,6 +209,13 @@ class RequestHandler extends APIHandlerBase {
209209 data : z . array ( z . object ( { type : z . string ( ) , id : z . union ( [ z . string ( ) , z . number ( ) ] ) } ) ) ,
210210 } ) ;
211211
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+
212219 // all known types and their metadata
213220 private typeMap : Record < string , ModelInfo > ;
214221
@@ -309,8 +316,29 @@ class RequestHandler extends APIHandlerBase {
309316
310317 let match = this . urlPatterns . collection . match ( path ) ;
311318 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+ }
314342 }
315343
316344 match = this . urlPatterns . relationship . match ( path ) ;
@@ -809,6 +837,90 @@ class RequestHandler extends APIHandlerBase {
809837 } ;
810838 }
811839
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+
812924 private async processRelationshipCRUD (
813925 prisma : DbClientContract ,
814926 mode : 'create' | 'update' | 'delete' ,
@@ -1296,6 +1408,24 @@ class RequestHandler extends APIHandlerBase {
12961408 return idFields . map ( ( idf ) => item [ idf . name ] ) . join ( this . idDivider ) ;
12971409 }
12981410
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+
12991429 private includeRelationshipIds ( model : string , args : any , mode : 'select' | 'include' ) {
13001430 const typeInfo = this . typeMap [ model ] ;
13011431 if ( ! typeInfo ) {
0 commit comments