@@ -61,9 +61,9 @@ export type RestApiHandlerOptions<Schema extends SchemaDef = SchemaDef> = {
6161 modelNameMapping ?: Record < string , string > ;
6262
6363 /**
64- * Mapping from model names to unique field name to be used as resource's ID.
64+ * Mapping from model names to unique index fields names to be used as resource's ID.
6565 */
66- externalIdMapping ?: Record < string , string > ;
66+ externalIdMapping ?: Record < string , string | string [ ] > ;
6767} ;
6868
6969type RelationshipInfo = {
@@ -261,7 +261,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
261261 private urlPatternMap : Record < UrlPatterns , UrlPattern > ;
262262 private modelNameMapping : Record < string , string > ;
263263 private reverseModelNameMapping : Record < string , string > ;
264- private externalIdMapping : Record < string , string > ;
264+ private externalIdMapping : Record < string , string | string [ ] > ;
265265
266266 constructor ( private readonly options : RestApiHandlerOptions < Schema > ) {
267267 this . validateOptions ( options ) ;
@@ -297,7 +297,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
297297 idDivider : z . string ( ) . min ( 1 ) . optional ( ) ,
298298 urlSegmentCharset : z . string ( ) . min ( 1 ) . optional ( ) ,
299299 modelNameMapping : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
300- externalIdMapping : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
300+ externalIdMapping : z . record ( z . string ( ) , z . union ( [ z . string ( ) , z . array ( z . string ( ) ) . min ( 1 ) ] ) ) . optional ( ) ,
301301 } ) ;
302302 const parseResult = schema . safeParse ( options ) ;
303303 if ( ! parseResult . success ) {
@@ -1335,24 +1335,53 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
13351335 const modelDef = this . requireModel ( model ) ;
13361336 const modelLower = lowerCaseFirst ( model ) ;
13371337 if ( ! ( modelLower in this . externalIdMapping ) ) {
1338- return Object . values ( modelDef . fields ) . filter ( ( f ) => modelDef . idFields . includes ( f . name ) ) ;
1339- }
1340-
1341- // map external ID name to unique constraint field
1342- const externalIdName = this . externalIdMapping [ modelLower ] ;
1343- for ( const [ name , info ] of Object . entries ( modelDef . uniqueFields ) ) {
1344- if ( name === externalIdName ) {
1345- if ( typeof info . type === 'string' ) {
1346- // single unique field
1347- return [ this . requireField ( model , info . type ) ] ;
1348- } else {
1338+ return Object . values ( modelDef . fields ) . filter (
1339+ ( f ) => ( f as FieldDef ) . name && modelDef . idFields . includes ( ( f as FieldDef ) . name ) ,
1340+ ) as FieldDef [ ] ;
1341+ }
1342+
1343+ // map external ID name or columns to unique constraint field(s)
1344+ const externalId = this . externalIdMapping [ modelLower ] ;
1345+ let resolved : FieldDef [ ] | undefined ;
1346+ if ( typeof externalId === 'string' ) {
1347+ // Try to match unique key name first
1348+ for ( const [ name , info ] of Object . entries ( modelDef . uniqueFields as Record < string , unknown > ) ) {
1349+ const infoTyped = info as any ;
1350+ if ( name === externalId ) {
1351+ if ( typeof infoTyped . type === 'string' ) {
1352+ // single unique field
1353+ resolved = [ this . requireField ( model , name ) ] ;
1354+ break ;
1355+ }
13491356 // compound unique fields
1350- return Object . keys ( info ) . map ( ( f ) => this . requireField ( model , f ) ) ;
1357+ resolved = Object . keys ( infoTyped ) . map ( ( f ) => this . requireField ( model , f ) ) ;
1358+ break ;
13511359 }
13521360 }
1353- }
1354-
1355- throw new Error ( `Model ${ model } does not have unique key ${ externalIdName } ` ) ;
1361+ // If not found, treat as a single column name (for single-column unique index with underscores)
1362+ if ( modelDef . fields [ externalId ] ) {
1363+ resolved = [ this . requireField ( model , externalId ) ] ;
1364+ }
1365+ } else if ( Array . isArray ( externalId ) ) {
1366+ // Array of column names (for compound or single unique index)
1367+ resolved = externalId . map ( ( col ) => this . requireField ( model , col ) ) ;
1368+ }
1369+
1370+ if ( ! resolved ) {
1371+ throw new Error ( `Invalid externalIdMapping for model ${ model } ` ) ;
1372+ }
1373+ const resolvedNames = resolved . map ( ( f ) => f . name ) ;
1374+ const uniqueSets = this . getUniqueFieldSets ( model ) ;
1375+ const isUnique =
1376+ uniqueSets . some (
1377+ ( set ) => set . length === resolvedNames . length && set . every ( ( f , i ) => f === resolvedNames [ i ] ) ,
1378+ ) ||
1379+ ( modelDef . idFields . length === resolvedNames . length &&
1380+ modelDef . idFields . every ( ( f , i ) => f === resolvedNames [ i ] ) ) ;
1381+ if ( ! isUnique ) {
1382+ throw new Error ( `Model ${ model } externalIdMapping must reference unique fields` ) ;
1383+ }
1384+ return resolved ;
13561385 }
13571386
13581387 private requireField ( model : string , field : string ) : FieldDef {
@@ -1366,7 +1395,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
13661395
13671396 private buildTypeMap ( ) {
13681397 this . typeMap = { } ;
1369- for ( const [ model , { fields } ] of Object . entries ( this . schema . models ) ) {
1398+ for ( const [ model , modelVal ] of Object . entries ( this . schema . models as Record < string , unknown > ) ) {
1399+ const fields = ( modelVal as any ) . fields as Record < string , FieldDef > ;
13701400 const idFields = this . getIdFields ( model ) ;
13711401 if ( idFields . length === 0 ) {
13721402 log ( this . options . log , 'warn' , `Not including model ${ model } in the API because it has no ID field` ) ;
@@ -1380,7 +1410,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
13801410 fields,
13811411 } ) ;
13821412
1383- for ( const [ field , fieldInfo ] of Object . entries ( fields ) ) {
1413+ for ( const [ field , fieldInfoRaw ] of Object . entries ( fields ) ) {
1414+ const fieldInfo = fieldInfoRaw as any ;
13841415 if ( ! fieldInfo . relation ) {
13851416 continue ;
13861417 }
0 commit comments