@@ -35,7 +35,7 @@ export interface FieldRow { name: string; type: string; isArray: boolean; requir
3535
3636/** Render an attr value for display: an object (e.g. view.badge @variantMap) as
3737 * ordered `k: v` pairs, an array as a comma list, else the scalar. */
38- function fmtAttrValue ( v : unknown ) : string {
38+ export function fmtAttrValue ( v : unknown ) : string {
3939 if ( v !== null && typeof v === "object" && ! Array . isArray ( v ) ) {
4040 return Object . entries ( v as Record < string , unknown > )
4141 . sort ( ( [ a ] , [ b ] ) => ( a < b ? - 1 : a > b ? 1 : 0 ) )
@@ -45,15 +45,45 @@ function fmtAttrValue(v: unknown): string {
4545 if ( Array . isArray ( v ) ) return v . map ( String ) . join ( ", " ) ;
4646 return String ( v ) ;
4747}
48+
49+ /** A node's authored attrs not already rendered by a specific renderer, as
50+ * escaped key/value pairs — consumed so they don't surface as "not rendered by
51+ * any page" in coverage. `skip` names the attrs a specific renderer handles.
52+ * Deterministic (sorted by name). This is the generic catch-all that documents
53+ * a consumer's own attrs (e.g. object.@dataflow) + the structural ones a
54+ * bespoke renderer doesn't (e.g. relationship.@onDelete). */
55+ function otherAttrs ( node : MetaData , cov : CoverageTracker , skip : Set < string > ) : ViewAttr [ ] {
56+ return [ ...node . ownAttrs ( ) ]
57+ . filter ( ( [ n ] ) => ! skip . has ( n ) )
58+ . sort ( ( [ a ] , [ b ] ) => ( a < b ? - 1 : a > b ? 1 : 0 ) )
59+ . map ( ( [ n , v ] ) => { cov . consumeAttr ( node , n ) ; return { name : esc ( n ) , value : esc ( fmtAttrValue ( v ) ) } ; } ) ;
60+ }
61+
62+ /** Consume a node + its whole subtree (nodes + attrs) so a section that renders
63+ * only a summary (e.g. a grid layout's top-level attrs) doesn't leave its nested
64+ * config flagged as un-rendered. */
65+ function consumeSubtree ( node : MetaData , cov : CoverageTracker ) : void {
66+ cov . consumeNode ( node ) ;
67+ for ( const [ n ] of node . ownAttrs ( ) ) cov . consumeAttr ( node , n ) ;
68+ for ( const c of node . ownChildren ( ) ) consumeSubtree ( c , cov ) ;
69+ }
70+
4871export interface IndexRow { name : string ; kind : string ; fields : string ; extra : string ; unique : boolean ; }
4972export interface ValidatorRow { scope : "field" | "object" ; subject : string ; rule : string ; // human-readable, HTML-escaped
5073}
51- export interface RelationRow { name : string ; toName : string ; toHref : string ; cardinality : string ; }
52- export interface OriginRow { field : string ; from : string ; via : string ; }
74+ export interface RelationRow { name : string ; toName : string ; toHref : string ; cardinality : string ; attrs : ViewAttr [ ] ; }
75+ export interface OriginRow { field : string ; from : string ; via : string ; attrs : ViewAttr [ ] ; }
76+ export interface LayoutRow { kind : string ; attrs : ViewAttr [ ] ; }
77+
78+ // Field attrs fieldRow renders specifically (as their own badges / enum / ref);
79+ // everything else authored on a field (e.g. @column, @storage) is rendered
80+ // generically as a badge so it isn't flagged "not rendered" in coverage.
81+ const FIELD_KNOWN_ATTRS = new Set ( [ "required" , "deprecated" , "maxLength" , "dbColumnType" , "default" , "xmlText" , "values" , "objectRef" , "description" ] ) ;
5382export interface HierRow { name : string ; href : string ; level : number ; self : boolean ; }
5483export interface ObjectPageData {
5584 name : string ; kindBadge : string ; isAbstract : boolean ; isView : boolean ; generation : string ;
5685 pkg : string ; href : string ; breadcrumbHtml : string ; desc : string ; tableName ?: string | undefined ; pkHtml ?: string | undefined ;
86+ objectAttrs : ViewAttr [ ] ; storageAttrs : ViewAttr [ ] ; layouts : LayoutRow [ ] ;
5787 ownFields : FieldRow [ ] ; inheritedFields : FieldRow [ ] ; indexes : IndexRow [ ] ; validators : ValidatorRow [ ] ;
5888 relations : RelationRow [ ] ; origins : OriginRow [ ] ; hierarchy : HierRow [ ] ; inheritanceMermaid ?: string | undefined ;
5989 neighborhoodMermaid ?: string | undefined ; neighborhoodLegend ?: { pkg : string ; fill : string ; stroke : string } [ ] | undefined ; neighborhoodMore ?: number | undefined ;
@@ -115,6 +145,12 @@ function fieldRow(f: MetaData, ownerHref: string, g: LinkGraph, cov: CoverageTra
115145 . map ( ( [ an , av ] ) => { cov . consumeAttr ( v , an ) ; return { name : esc ( an ) , value : esc ( fmtAttrValue ( av ) ) } ; } ) ;
116146 views . push ( { subType : esc ( v . subType ) , attrs : vAttrs } ) ;
117147 }
148+ // any other authored field attrs (e.g. @column DB name, @storage) as badges
149+ for ( const [ n , val ] of [ ...f . ownAttrs ( ) ] . sort ( ( [ x ] , [ y ] ) => ( x < y ? - 1 : x > y ? 1 : 0 ) ) ) {
150+ if ( FIELD_KNOWN_ATTRS . has ( n ) ) continue ;
151+ cov . consumeAttr ( f , n ) ;
152+ bits . push ( badge ( { text : `@${ n } =${ fmtAttrValue ( val ) } ` , cls : "badge-soft badge-ghost" } ) ) ;
153+ }
118154 const desc = esc ( a ( "description" ) ?? "" ) ;
119155 return { name : f . name , type : f . subType , isArray : f . resolvedIsArray ( ) , required, badgesHtml : bits . join ( " " ) , desc, enumValues, views, refHref, refName, inheritedFrom : undefined , anchor : `f-${ f . name } ` } ;
120156}
@@ -136,6 +172,8 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
136172 const o = dn . node ;
137173 cov . consumeNode ( o ) ;
138174 cov . consumeAttr ( o , "description" ) ;
175+ // consumer/structural attrs authored on the object itself (e.g. @dataflow, @neo4j)
176+ const objectAttrs = otherAttrs ( o , cov , new Set ( [ "description" ] ) ) ;
139177
140178 // inheritance hierarchy rows (ancestors nearest-last so level increases downward) + self + direct children
141179 const anc = g . ancestors ( fqn ) ; // nearest-first
@@ -150,27 +188,32 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
150188
151189 // storage: table vs view, generation, pk
152190 let tableName : string | undefined , isView = false , pkHtml : string | undefined , generation = "" ;
191+ const storageAttrs : ViewAttr [ ] = [ ] ;
153192 for ( const s of o . childrenOfType ( "source" ) ) {
154193 cov . consumeNode ( s ) ;
155194 const t = s . attr ( "table" ) ; if ( t !== undefined ) { cov . consumeAttr ( s , "table" ) ; tableName = String ( t ) ; }
156195 const kind = s . attr ( "kind" ) ; if ( kind !== undefined ) { cov . consumeAttr ( s , "kind" ) ; if ( String ( kind ) === "view" ) isView = true ; }
196+ // extra source attrs — e.g. a view source's @view (view name) — rendered in the storage section
197+ storageAttrs . push ( ...otherAttrs ( s , cov , new Set ( [ "table" , "kind" ] ) ) ) ;
157198 }
158199 // indexes: identity (pk/secondary) + index.lookup with tuning detail
159200 const indexes : IndexRow [ ] = [ ] ;
160201 for ( const id of o . childrenOfType ( "identity" ) ) {
161202 cov . consumeNode ( id ) ;
162203 const flds = id . attr ( "fields" ) ; if ( flds !== undefined ) cov . consumeAttr ( id , "fields" ) ;
163204 const fields = Array . isArray ( flds ) ? flds . join ( ", " ) : String ( flds ?? "" ) ;
205+ // other authored identity attrs (e.g. @constraintName) rendered into the detail cell
206+ const idExtra = otherAttrs ( id , cov , new Set ( [ "fields" , "references" , "enforce" , "generation" ] ) ) . map ( ( x ) => `@${ x . name } =${ x . value } ` ) . join ( " · " ) ;
164207 if ( id . subType === "primary" ) {
165208 pkHtml = `<code>${ esc ( fields ) } </code>` ;
166209 const gen = id . attr ( "generation" ) ; if ( gen !== undefined ) { cov . consumeAttr ( id , "generation" ) ; generation = String ( gen ) ; }
167- indexes . push ( { name : id . name , kind : "primary" , fields, extra : "" , unique : true } ) ;
210+ indexes . push ( { name : id . name , kind : "primary" , fields, extra : idExtra , unique : true } ) ;
168211 } else if ( id . subType === "secondary" ) {
169- indexes . push ( { name : id . name , kind : "unique" , fields, extra : "" , unique : true } ) ;
212+ indexes . push ( { name : id . name , kind : "unique" , fields, extra : idExtra , unique : true } ) ;
170213 } else if ( id . subType === "reference" ) {
171214 const ref = id . attr ( "references" ) ; if ( ref !== undefined ) cov . consumeAttr ( id , "references" ) ;
172215 const enf = id . attr ( "enforce" ) ; if ( enf !== undefined ) cov . consumeAttr ( id , "enforce" ) ;
173- indexes . push ( { name : id . name , kind : "fk" , fields, extra : [ ref ? `→ ${ esc ( String ( ref ) ) } ` : "" , enf === false || enf === "false" ? "logical" : "" ] . filter ( Boolean ) . join ( " · " ) , unique : false } ) ;
216+ indexes . push ( { name : id . name , kind : "fk" , fields, extra : [ ref ? `→ ${ esc ( String ( ref ) ) } ` : "" , enf === false || enf === "false" ? "logical" : "" , idExtra ] . filter ( Boolean ) . join ( " · " ) , unique : false } ) ;
174217 }
175218 }
176219 for ( const ix of o . childrenOfType ( "index" ) ) {
@@ -201,17 +244,38 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
201244 }
202245 validators . sort ( ( a , b ) => a . subject . localeCompare ( b . subject ) ) ;
203246
204- // relationships
247+ // relationships — bespoke name/target/cardinality + generic extras (@onDelete/@onUpdate/…)
248+ const relAttrs = new Map < string , ViewAttr [ ] > ( ) ;
249+ for ( const rel of o . childrenOfType ( "relationship" ) ) {
250+ cov . consumeNode ( rel ) ; cov . consumeAttr ( rel , "objectRef" ) ; cov . consumeAttr ( rel , "cardinality" ) ;
251+ relAttrs . set ( rel . name , otherAttrs ( rel , cov , new Set ( [ "objectRef" , "cardinality" , "through" , "sourceRefField" , "symmetric" ] ) ) ) ;
252+ }
205253 const relations : RelationRow [ ] = g . relationshipsOf ( fqn ) . map ( ( r ) => {
206254 const t = g . byFqn ( r . toFqn ) ;
207- return { name : r . name , toName : t ?. name ?? r . toFqn , toHref : t ? g . relHref ( dn . href , t . href ) : "" , cardinality : r . cardinality } ;
255+ return { name : r . name , toName : t ?. name ?? r . toFqn , toHref : t ? g . relHref ( dn . href , t . href ) : "" , cardinality : r . cardinality , attrs : relAttrs . get ( r . name ) ?? [ ] } ;
208256 } ) ;
209- for ( const rel of o . childrenOfType ( "relationship" ) ) { cov . consumeNode ( rel ) ; cov . consumeAttr ( rel , "objectRef" ) ; cov . consumeAttr ( rel , "cardinality" ) ; }
210257
211- // origin provenance
212- const origins : OriginRow [ ] = g . originsOf ( fqn ) . map ( ( r ) => ( { field : r . field , from : esc ( r . from ) , via : esc ( r . via ) } ) )
258+ // origin provenance — bespoke field/from/via + generic extras (@of/@agg/@filter/…)
259+ const originAttrs = new Map < string , ViewAttr [ ] > ( ) ;
260+ for ( const f of o . childrenOfType ( "field" ) ) for ( const org of f . childrenOfType ( "origin" ) ) {
261+ cov . consumeNode ( org ) ; cov . consumeAttr ( org , "from" ) ; cov . consumeAttr ( org , "via" ) ;
262+ const cur = originAttrs . get ( f . name ) ?? [ ] ;
263+ cur . push ( ...otherAttrs ( org , cov , new Set ( [ "from" , "via" ] ) ) ) ;
264+ originAttrs . set ( f . name , cur ) ;
265+ }
266+ const origins : OriginRow [ ] = g . originsOf ( fqn ) . map ( ( r ) => ( { field : r . field , from : esc ( r . from ) , via : esc ( r . via ) , attrs : originAttrs . get ( r . field ) ?? [ ] } ) )
213267 . sort ( ( a , b ) => a . field . localeCompare ( b . field ) ) ;
214- for ( const f of o . childrenOfType ( "field" ) ) for ( const org of f . childrenOfType ( "origin" ) ) { cov . consumeNode ( org ) ; cov . consumeAttr ( org , "from" ) ; cov . consumeAttr ( org , "via" ) ; }
268+
269+ // grid/form layout (admin-ui presentation on projections): render the layout node's
270+ // own attrs (@pageSize/@defaultSortOrder/…); consume nested column config so it
271+ // isn't flagged un-rendered.
272+ const layouts : LayoutRow [ ] = [ ] ;
273+ for ( const l of o . childrenOfType ( "layout" ) ) {
274+ cov . consumeNode ( l ) ;
275+ const attrs = otherAttrs ( l , cov , new Set ( ) ) ;
276+ for ( const c of l . ownChildren ( ) ) consumeSubtree ( c , cov ) ;
277+ layouts . push ( { kind : esc ( l . subType ) , attrs } ) ;
278+ }
215279
216280 // fields own vs inherited
217281 const ownNames = new Set ( o . ownChildren ( ) . filter ( ( c ) => c . type === "field" ) . map ( ( c ) => c . name ) ) ;
@@ -282,7 +346,7 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
282346 return {
283347 name : dn . name , kindBadge : o . subType , isAbstract : o . isAbstract , isView, generation,
284348 pkg : dn . pkg , href : dn . href , breadcrumbHtml : crumbs . join ( " / " ) , desc : esc ( o . attr ( "description" ) ?? "" ) ,
285- tableName, pkHtml, ownFields, inheritedFields, indexes, validators, relations, origins,
349+ tableName, pkHtml, objectAttrs , storageAttrs , layouts , ownFields, inheritedFields, indexes, validators, relations, origins,
286350 hierarchy, inheritanceMermaid, neighborhoodMermaid, neighborhoodLegend, neighborhoodMore, referencedBy, references, usedByTemplates, sourceFile : src ,
287351 } ;
288352}
0 commit comments