Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app/admin-annotations.cds
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,10 @@ annotate AdminService.Tutorials with @UI: {
Target: 'freshnessFindings/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Images', ID: 'MediaImagesFacet', Target: 'images/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Assets', ID: 'MediaAssetsFacet', Target: 'assets/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Knowledge Graph', ID: 'KgFieldsFacet', Target: '@UI.FieldGroup#KnowledgeGraph' },
{ $Type: 'UI.ReferenceFacet', Label: 'Concepts Taught', ID: 'ConceptsTaughtFacet', Target: 'conceptLinks/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Co-Completed', ID: 'CoCompletionsFacet', Target: 'coCompletions/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Community', ID: 'KgCommunityFacet', Target: 'communityMembership/@UI.LineItem' },
{ $Type: 'UI.CollectionFacet', ID: 'Feedback', Label: 'Feedback', Facets: [
{ $Type: 'UI.ReferenceFacet', ID: 'FeedbackSummary',
Target: 'feedbackSummary/@UI.FieldGroup#FeedbackSummary',
Expand Down Expand Up @@ -4432,3 +4436,41 @@ annotate AdminService.TutorialAssets with @(
{ Value: contentHash, Label: 'Hash' }
]
);

// --- Knowledge Graph facets (task-3) ---
// TutorialConceptLinks LineItem: concept FK, predicate (teaches|extends), confidence score.
annotate AdminService.TutorialConceptLinks with @(
UI.LineItem: [
{ Value: concept_ID, Label: 'Concept' },
{ Value: predicate, Label: 'Relation' },
{ Value: confidence, Label: 'Confidence' }
]
);

// CoCompletions LineItem: target tutorial slug + co-completion score.
annotate AdminService.CoCompletions with @(
UI.LineItem: [
{ Value: targetSlug, Label: 'Also Completed' },
{ Value: score, Label: 'Score' }
]
);

// KgCommunityMembers LineItem: community id + fingerprint for the OP facet.
annotate AdminService.KgCommunityMembers with @(
UI.LineItem: [
{ Value: communityId, Label: 'Community ID' },
{ Value: communityFingerprint, Label: 'Fingerprint' },
{ Value: vertexType, Label: 'Type' }
]
);

// FieldGroup for PageRank score — shown in KgFieldsFacet on Tutorials OP.
// rank is a to-one Association (slug-joined) added in task-1.
annotate AdminService.Tutorials with @(
UI.FieldGroup #KnowledgeGraph: {
Label: 'Knowledge Graph',
Data: [
{ $Type: 'UI.DataField', Value: rank.score, Label: 'PageRank Score' }
]
}
);
16 changes: 16 additions & 0 deletions srv/admin-service.cds
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ service AdminService {
// Filtered to channel='prod' so the admin OP shows prod-published media.
images : Association to many TutorialImages on images.slug = $self.slug and images.channel = 'prod',
assets : Association to many TutorialAssets on assets.slug = $self.slug and assets.channel = 'prod',
// KG facet (task-1): rank + co-completions associations.
// conceptLinks already carried by '*' (db/knowledge-graph.cds extends base.Tutorials).
rank : Association to one TutorialRank on rank.slug = $self.slug,
coCompletions : Association to many CoCompletions on coCompletions.sourceSlug = $self.slug,
// KG facet (task-2): community membership — links this tutorial to its
// Louvain-detected community rows (slug-based join). From the OP, a $expand
// on communityMembership exposes communityId + communityFingerprint, which
// can be used to fetch the label from KgCommunityLabel. No schema change:
// KgCommunityMembers is an @readonly projection on ims.KgCommunity, which
// already carries the slug column.
communityMembership : Association to many AdminService.KgCommunityMembers on communityMembership.slug = $self.slug,
};
// Filtered picklist for redirectTo value help — only ACTIVE tutorials can be redirect targets
@readonly
Expand Down Expand Up @@ -634,6 +645,11 @@ service AdminService {
@readonly @cds.redirection.target: false entity TutorialImages as projection on ims.TutorialImages;
@readonly @cds.redirection.target: false entity TutorialAssets as projection on ims.TutorialAssets;

// KG facet (task-1): read-only projections used by the Tutorials admin OP KG tab.
@readonly @cds.redirection.target: false entity TutorialConceptLinks as projection on ims.TutorialConceptLinks;
@readonly @cds.redirection.target: false entity TutorialRank as projection on ims.TutorialRank;
@readonly @cds.redirection.target: false entity CoCompletions as projection on ims.CoCompletions;

// Issue #622 — read-only recipient list for the "Last Chance Emails"
// admin section. Powers the dropdown for sendLastChanceEmail and the
// preview list for sendLastChanceEmailsAllDormant. One row per
Expand Down
17 changes: 17 additions & 0 deletions test/hybrid/kg-facet.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect, beforeAll } from 'vitest'
import cds from '@sap/cds'

cds.test('serve', '--project', '.', '--profile', 'hybrid')

describe('KG facet reads (hybrid)', () => {
let admin
beforeAll(async () => { admin = await cds.connect.to('AdminService') })
it('reads a tutorial with KG associations expanded without error', async () => {
const t = await admin.run(SELECT.one.from('AdminService.Tutorials').columns('ID', 'slug'))
expect(t).toBeTruthy()
const links = await admin.run(SELECT.from('AdminService.TutorialConceptLinks').where({ tutorial_ID: t.ID }))
expect(Array.isArray(links)).toBe(true) // may be empty in DEV — that's fine
const co = await admin.run(SELECT.from('AdminService.CoCompletions').limit(1))
expect(Array.isArray(co)).toBe(true)
})
})
21 changes: 21 additions & 0 deletions test/unit/annotations-kg.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect, beforeAll } from 'vitest'
import cds from '@sap/cds'

describe('Knowledge Graph facets', () => {
let m
beforeAll(async () => { m = await cds.load('*') })

it('OP facets include KG facets', () => {
const ids = m.definitions['AdminService.Tutorials']['@UI.Facets'].map((f) => f.ID)
expect(ids).toContain('ConceptsTaughtFacet')
expect(ids).toContain('CoCompletionsFacet')
expect(ids).toContain('KgCommunityFacet')
})

it('concept links LineItem shows predicate + confidence', () => {
const li = m.definitions['AdminService.TutorialConceptLinks']['@UI.LineItem']
const vals = li.map((x) => x.Value?.['='] || x.Value)
expect(vals).toContain('predicate')
expect(vals).toContain('confidence')
})
})
13 changes: 13 additions & 0 deletions test/unit/kg-community-link.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, it, expect, beforeAll } from 'vitest'
import cds from '@sap/cds'

describe('community label reachable', () => {
let m
beforeAll(async () => {
m = await cds.load('*')
})
it('Tutorials exposes community membership or virtual label', () => {
const t = m.definitions['AdminService.Tutorials'].elements
expect(t.communityMembership || t.communityLabel).toBeTruthy()
})
})
17 changes: 17 additions & 0 deletions test/unit/kg-exposure.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// test/unit/kg-exposure.test.js
import { describe, it, expect, beforeAll } from 'vitest'
import cds from '@sap/cds'
describe('KG exposure on AdminService', () => {
let m; beforeAll(async () => { m = await cds.load('*') })
it('exposes concept links, rank, co-completions read-only', () => {
expect(m.definitions['AdminService.TutorialConceptLinks']).toBeTruthy()
expect(m.definitions['AdminService.TutorialRank']).toBeTruthy()
expect(m.definitions['AdminService.CoCompletions']).toBeTruthy()
})
it('Tutorials carries conceptLinks / rank / coCompletions', () => {
const t = m.definitions['AdminService.Tutorials'].elements
expect(t.conceptLinks).toBeTruthy()
expect(t.rank).toBeTruthy()
expect(t.coCompletions).toBeTruthy()
})
})
Loading