11import { formatDocument , ZModelCodeGenerator } from '@zenstackhq/language' ;
2- import { DataModel , Enum , type Model } from '@zenstackhq/language/ast' ;
2+ import { DataModel , Enum , isDataField , type DataField , type Model } from '@zenstackhq/language/ast' ;
33import colors from 'colors' ;
44import fs from 'node:fs' ;
55import path from 'node:path' ;
@@ -14,7 +14,7 @@ import {
1414} from './action-utils' ;
1515import { consolidateEnums , syncEnums , syncRelation , syncTable , type Relation } from './pull' ;
1616import { providers as pullProviders } from './pull/provider' ;
17- import { getDatasource , getDbName , getRelationFieldsKey , getRelationFkName , isDatabaseManagedAttribute } from './pull/utils' ;
17+ import { getDatasource , getDbName , getRelationFieldsKey , getRelationFkName , getRelationName , isDatabaseManagedAttribute } from './pull/utils' ;
1818import type { DataSourceProviderType } from '@zenstackhq/schema' ;
1919import { CliError } from '../cli-error' ;
2020
@@ -35,6 +35,25 @@ export type PullOptions = {
3535 indent : number ;
3636} ;
3737
38+ function hasRelationFieldsArg ( field : DataField ) {
39+ const relationAttr = field . attributes . find ( ( a ) => a . decl . ref ?. name === '@relation' ) ;
40+ return ! ! relationAttr ?. args . some ( ( a ) => a . name === 'fields' ) ;
41+ }
42+
43+ function getReferencedModelName ( field : DataField ) {
44+ return field . type . reference ?. ref ? getDbName ( field . type . reference . ref ) : undefined ;
45+ }
46+
47+ function matchesRelationNameFallback ( field : DataField , relationName : string , candidate : DataField ) {
48+ const referencedModelName = getReferencedModelName ( field ) ;
49+ return (
50+ ! ! referencedModelName &&
51+ getRelationName ( candidate ) === relationName &&
52+ hasRelationFieldsArg ( candidate ) === hasRelationFieldsArg ( field ) &&
53+ getReferencedModelName ( candidate ) === referencedModelName
54+ ) ;
55+ }
56+
3857/**
3958 * CLI action for db related commands
4059 */
@@ -283,46 +302,52 @@ async function runPull(options: PullOptions) {
283302 }
284303
285304 newDataModel . fields . forEach ( ( f ) => {
286- // Prioritized matching: exact db name > relation fields key > relation FK name > type reference
305+ // Prioritized matching: exact db name > relation fields key > relation FK name > relation name > type reference
287306 let originalFields = originalDataModel . fields . filter ( ( d ) => getDbName ( d ) === getDbName ( f ) ) ;
288307
289- // If this is a back-reference relation field (has @relation but no `fields` arg), silently skip
290- const isRelationField =
291- f . $type === 'DataField' && ! ! ( f as any ) . attributes ?. some ( ( a : any ) => a ?. decl ?. ref ?. name === '@relation' ) ;
292- if ( originalFields . length === 0 && isRelationField && ! getRelationFieldsKey ( f as any ) ) {
293- return ;
294- }
295-
296308 if ( originalFields . length === 0 ) {
297309 // Try matching by relation fields key (the `fields` attribute in @relation)
298310 // This matches relation fields by their FK field references
299- const newFieldsKey = getRelationFieldsKey ( f as any ) ;
311+ const newFieldsKey = isDataField ( f ) ? getRelationFieldsKey ( f ) : undefined ;
300312 if ( newFieldsKey ) {
301313 originalFields = originalDataModel . fields . filter (
302- ( d ) => getRelationFieldsKey ( d as any ) === newFieldsKey ,
314+ ( d ) => isDataField ( d ) && getRelationFieldsKey ( d ) === newFieldsKey ,
303315 ) ;
304316 }
305317 }
306318
307319 if ( originalFields . length === 0 ) {
308320 // Try matching by relation FK name (the `map` attribute in @relation)
309- originalFields = originalDataModel . fields . filter (
310- ( d ) =>
311- getRelationFkName ( d as any ) === getRelationFkName ( f as any ) &&
312- ! ! getRelationFkName ( d as any ) &&
313- ! ! getRelationFkName ( f as any ) ,
314- ) ;
321+ const newFkName = isDataField ( f ) ? getRelationFkName ( f ) : undefined ;
322+ if ( newFkName ) {
323+ originalFields = originalDataModel . fields . filter (
324+ ( d ) => isDataField ( d ) && getRelationFkName ( d ) === newFkName ,
325+ ) ;
326+ }
327+ }
328+
329+ if ( originalFields . length === 0 ) {
330+ // Try matching by relation name (the `name` arg in @relation)
331+ // This is essential for back-reference fields that only have a relation name
332+ const newRelName = isDataField ( f ) ? getRelationName ( f ) : undefined ;
333+ if ( newRelName ) {
334+ originalFields = originalDataModel . fields . filter (
335+ ( d ) =>
336+ isDataField ( d ) &&
337+ isDataField ( f ) &&
338+ matchesRelationNameFallback ( f , newRelName , d ) ,
339+ ) ;
340+ }
315341 }
316342
317343 if ( originalFields . length === 0 ) {
318344 // Try matching by type reference
319345 // We need this because for relations that don't have @relation, we can only check if the original exists by the field type.
320346 // Yes, in this case it can potentially result in multiple original fields, but we only want to ensure that at least one relation exists.
321- // In the future, we might implement some logic to detect how many of these types of relations we need and add/remove fields based on this.
322347 originalFields = originalDataModel . fields . filter (
323348 ( d ) =>
324- f . $type === 'DataField' &&
325- d . $type === 'DataField' &&
349+ isDataField ( f ) &&
350+ isDataField ( d ) &&
326351 f . type . reference ?. ref &&
327352 d . type . reference ?. ref &&
328353 getDbName ( f . type . reference . ref ) === getDbName ( d . type . reference . ref ) ,
@@ -332,7 +357,7 @@ async function runPull(options: PullOptions) {
332357 if ( originalFields . length > 1 ) {
333358 // If this is a back-reference relation field (no `fields` attribute),
334359 // silently skip when there are multiple potential matches
335- const isBackReferenceField = ! getRelationFieldsKey ( f as any ) ;
360+ const isBackReferenceField = isDataField ( f ) && ! getRelationFieldsKey ( f ) ;
336361 if ( ! isBackReferenceField ) {
337362 console . warn (
338363 colors . yellow (
@@ -499,31 +524,43 @@ async function runPull(options: PullOptions) {
499524 } ) ;
500525 originalDataModel . fields
501526 . filter ( ( f ) => {
502- // Prioritized matching: exact db name > relation fields key > relation FK name > type reference
527+ // Prioritized matching: exact db name > relation fields key > relation FK name > relation name > type reference
503528 const matchByDbName = newDataModel . fields . find ( ( d ) => getDbName ( d ) === getDbName ( f ) ) ;
504529 if ( matchByDbName ) return false ;
505530
506531 // Try matching by relation fields key (the `fields` attribute in @relation)
507- const originalFieldsKey = getRelationFieldsKey ( f as any ) ;
532+ const originalFieldsKey = isDataField ( f ) ? getRelationFieldsKey ( f ) : undefined ;
508533 if ( originalFieldsKey ) {
509534 const matchByFieldsKey = newDataModel . fields . find (
510- ( d ) => getRelationFieldsKey ( d as any ) === originalFieldsKey ,
535+ ( d ) => isDataField ( d ) && getRelationFieldsKey ( d ) === originalFieldsKey ,
511536 ) ;
512537 if ( matchByFieldsKey ) return false ;
513538 }
514539
515- const matchByFkName = newDataModel . fields . find (
516- ( d ) =>
517- getRelationFkName ( d as any ) === getRelationFkName ( f as any ) &&
518- ! ! getRelationFkName ( d as any ) &&
519- ! ! getRelationFkName ( f as any ) ,
520- ) ;
521- if ( matchByFkName ) return false ;
540+ const originalFkName = isDataField ( f ) ? getRelationFkName ( f ) : undefined ;
541+ if ( originalFkName ) {
542+ const matchByFkName = newDataModel . fields . find (
543+ ( d ) => isDataField ( d ) && getRelationFkName ( d ) === originalFkName ,
544+ ) ;
545+ if ( matchByFkName ) return false ;
546+ }
547+
548+ // Try matching by relation name (for named back-reference fields)
549+ const originalRelName = isDataField ( f ) ? getRelationName ( f ) : undefined ;
550+ if ( originalRelName ) {
551+ const matchByRelName = newDataModel . fields . find (
552+ ( d ) =>
553+ isDataField ( d ) &&
554+ isDataField ( f ) &&
555+ matchesRelationNameFallback ( f , originalRelName , d ) ,
556+ ) ;
557+ if ( matchByRelName ) return false ;
558+ }
522559
523560 const matchByTypeRef = newDataModel . fields . find (
524561 ( d ) =>
525- f . $type === 'DataField' &&
526- d . $type === 'DataField' &&
562+ isDataField ( f ) &&
563+ isDataField ( d ) &&
527564 f . type . reference ?. ref &&
528565 d . type . reference ?. ref &&
529566 getDbName ( f . type . reference . ref ) === getDbName ( d . type . reference . ref ) ,
0 commit comments