From 9933e4f00468490ba1d1f47699f1df0347aee9d4 Mon Sep 17 00:00:00 2001 From: Thomas Jung Date: Fri, 28 Aug 2026 16:42:46 -0400 Subject: [PATCH] fix(homepage): boot-seed HomepageShelves; retire seed CSV + manual seed:thirdparty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomepageShelves kept losing its curated/third-party rows (Prior Labs, Dremio, Reltio, n8n, …) on deploy. Root cause: while the seed CSV lived in db/data it compiled to an .hdbtabledata with include_filter:[] that HDI full-replaced the table from on every deploy whose CSV hash changed, wiping runtime-seeded rows and admin edits. Commit 1404c4c4 moved the CSV to test/data (stopping the wipe on PROD) but never re-seeded the lost rows, and the manual `npm run seed:thirdparty` promotion was DEV/PROD-ambiguous (it followed the CDS binding, so it could silently target the wrong environment). Replace both mechanisms with an idempotent, non-destructive boot seed: - srv/lib/homepage/homepage-shelves-defaults.js — single inline canonical baseline (SAP entries + third-party links); ships with the srv bundle, no runtime file-read / packaging risk. - srv/lib/homepage/seed-homepage-shelves.js — insert-if-missing on (verb,url), called from cds.on('served'). Never overwrites admin edits, self-heals a deleted baseline row on next restart, non-fatal, runs in every profile (cds watch, unit tests, hybrid, production). - Retire test/data seed CSV, db/data/staging/homepage-thirdparty.json, scripts/seed-thirdparty.js + its two tests, and the seed:thirdparty npm script (now a helpful error stub pointing at the new path). - admin-managed-seed-csvs guard test asserts the CSV is gone from BOTH data dirs. - Docs: new "HomepageShelves seeding" section in homepage.md. Tests: new defaults-validation + seed-behavior unit tests added; homepage-seed, homepage-shelves-crud, homepage-link-health still green (9078 unit tests pass). --- db/data/staging/homepage-thirdparty.json | 281 ---- docs/developers/architecture/homepage.md | 14 + package.json | 2 +- .../__tests__/seed-thirdparty-data.test.ts | 88 -- .../__tests__/seed-thirdparty-loader.test.ts | 48 - scripts/seed-thirdparty.js | 81 - srv/lib/homepage/homepage-shelves-defaults.js | 1297 +++++++++++++++++ srv/lib/homepage/seed-homepage-shelves.js | 45 + srv/server.js | 18 + ...com.sap.developers.ims-HomepageShelves.csv | 75 - test/unit/db/admin-managed-seed-csvs.test.js | 14 +- .../homepage-shelves-defaults.test.js | 59 + .../homepage/seed-homepage-shelves.test.js | 61 + 13 files changed, 1508 insertions(+), 575 deletions(-) delete mode 100644 db/data/staging/homepage-thirdparty.json delete mode 100644 scripts/__tests__/seed-thirdparty-data.test.ts delete mode 100644 scripts/__tests__/seed-thirdparty-loader.test.ts delete mode 100644 scripts/seed-thirdparty.js create mode 100644 srv/lib/homepage/homepage-shelves-defaults.js create mode 100644 srv/lib/homepage/seed-homepage-shelves.js delete mode 100644 test/data/com.sap.developers.ims-HomepageShelves.csv create mode 100644 test/unit/homepage/homepage-shelves-defaults.test.js create mode 100644 test/unit/homepage/seed-homepage-shelves.test.js diff --git a/db/data/staging/homepage-thirdparty.json b/db/data/staging/homepage-thirdparty.json deleted file mode 100644 index 4dbf7e77c..000000000 --- a/db/data/staging/homepage-thirdparty.json +++ /dev/null @@ -1,281 +0,0 @@ -[ - { - "_comment": "HOW TO PROMOTE TO PROD: after review on DEV, either (a) append these rows to db/data/com.sap.developers.ims-HomepageShelves.csv and redeploy PROD, or (b) run `npm run seed:thirdparty` against the PROD cds profile. This file stays the source of truth. Rows loaded to DEV via `npm run seed:thirdparty`. See docs/superpowers/specs/2026-07-30-homepage-thirdparty-content-design.md" - }, - { - "ID": "66333900-rpt1-0005-0001-000000000001", - "verb": "AI", "shelf": "START_HERE", "sortOrder": 26, - "title": "SAP RPT-1 Playground", - "url": "https://rpt.cloud.sap", - "description": "Try SAP's RPT-1 tabular foundation model in the browser — bring your own data or use SAP-provided example datasets.", - "badge": "NEW", "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Predict on your tables with RPT-1 — no training, right in the browser.", - "whyItMatters": "RPT-1 is SAP's semantics-aware foundation model for tabular data. The playground lets you run classification and regression on your own tables or SAP sample datasets without any setup — the fastest way to see what the model does.", - "personaTags": ["role:developer", "role:student", "deployment:cloud"] - }, - { - "ID": "66333900-rpt1-0005-0001-000000000002", - "verb": "AI", "shelf": "REFERENCE", "sortOrder": 35, - "title": "SAP RPT-1 on Hugging Face", - "url": "https://huggingface.co/SAP/sap-rpt-1-oss", - "description": "Open-source release of SAP RPT-1 — model card, weights, and usage examples for the tabular foundation model.", - "badge": "NEW", "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "The open-source RPT-1 model card, weights, and examples.", - "whyItMatters": "RPT-1 is published open-source on Hugging Face as a semantics-aware tabular in-context learner for classification and regression. The model card is the reference for developers integrating it into their own pipelines.", - "personaTags": ["role:developer", "role:student", "deployment:cloud"] - }, - { - "ID": "66333900-rpt1-1029-0001-000000000001", - "verb": "MODEL", "shelf": "REFERENCE", "sortOrder": 65, - "title": "SAP RPT-1", - "url": "https://rpt.cloud.sap", - "description": "SAP's relational/tabular foundation model — predict directly on structured data without task-specific training.", - "badge": "NEW", "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "A foundation model that predicts directly on your data models.", - "whyItMatters": "RPT-1 brings foundation-model prediction to structured, tabular data — the shape of most SAP business data. For teams modeling data in HANA Cloud or Datasphere, it is a fast path to classification and regression without building a bespoke ML pipeline.", - "personaTags": ["role:developer", "role:architect", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-0002-0001-000000000001", - "verb": "BUILD", "shelf": "TOOLS", "sortOrder": 200, - "title": "Vercel", - "url": "https://vercel.com", - "description": "Frontend cloud for building and deploying React, Vue, and static sites with global edge delivery.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Deploy React and Vue frontends to a global edge in minutes.", - "whyItMatters": "CAP treats React and Vue as first-class frontends. Vercel is a common host for those SPAs, giving developers preview deployments, edge functions, and CI-driven releases that pair well with a CAP backend on BTP.", - "personaTags": ["role:developer", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-1029-0001-000000000001", - "verb": "MODEL", "shelf": "REFERENCE", "sortOrder": 200, - "title": "Dremio", - "url": "https://www.dremio.com", - "description": "Lakehouse platform for SQL analytics directly on data lake storage, built around Apache Iceberg.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Query your data lake with SQL — no copies, no cubes.", - "whyItMatters": "Dremio is a leading open lakehouse engine. For teams federating SAP data with lake storage, it is a practical reference for Iceberg-based analytics alongside SAP Datasphere and Business Data Cloud.", - "personaTags": ["role:developer", "role:architect", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-1029-0001-000000000002", - "verb": "MODEL", "shelf": "KEEP_CURRENT", "sortOrder": 210, - "title": "Dremio Community", - "url": "https://community.dremio.com", - "description": "Community forum for Dremio users — Q&A, how-tos, and release discussion.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Ask questions and share patterns with other Dremio users.", - "whyItMatters": "The Dremio community forum is where lakehouse practitioners troubleshoot Iceberg, reflections, and federation — useful when integrating lake data with SAP analytics.", - "personaTags": ["role:developer", "role:architect"] - }, - { - "ID": "66333900-3rd0-1029-0001-000000000003", - "verb": "MODEL", "shelf": "REFERENCE", "sortOrder": 220, - "title": "Apache Iceberg", - "url": "https://iceberg.apache.org", - "description": "Open table format for huge analytic datasets — the storage standard behind modern lakehouses.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "The open table format underpinning modern lakehouses.", - "whyItMatters": "Iceberg is the table format SAP Business Data Cloud and many lake engines build on. Understanding it helps architects reason about how SAP and non-SAP data interoperate at the storage layer.", - "personaTags": ["role:developer", "role:architect", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-1029-0001-000000000004", - "verb": "MODEL", "shelf": "KEEP_CURRENT", "sortOrder": 230, - "title": "Data Engineering Weekly", - "url": "https://www.dataengineeringweekly.com", - "description": "Curated weekly newsletter on data engineering trends, tools, and architecture.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Stay current on the wider data-engineering ecosystem.", - "whyItMatters": "A concise weekly read that keeps data architects aware of trends beyond the SAP stack — pipelines, formats, and platform shifts that influence integration choices.", - "personaTags": ["role:architect"] - }, - { - "ID": "66333900-3rd0-1029-0001-000000000005", - "verb": "MODEL", "shelf": "KEEP_CURRENT", "sortOrder": 240, - "title": "The Data Stack Show", - "url": "https://datastackshow.com", - "description": "Podcast with data engineers and founders on how modern data stacks are built and run.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Conversations on how real data stacks get built.", - "whyItMatters": "Practitioner interviews that surface the tradeoffs behind modern data platforms — helpful context for teams positioning SAP data products within a broader stack.", - "personaTags": ["role:developer", "role:architect"] - }, - { - "ID": "66333900-3rd0-1029-0001-000000000006", - "verb": "MODEL", "shelf": "KEEP_CURRENT", "sortOrder": 250, - "title": "Reltio Community", - "url": "https://community.reltio.com", - "description": "Community for Reltio master-data-management practitioners — Q&A and best practices.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Master-data-management practices from the Reltio community.", - "whyItMatters": "MDM is a frequent companion to SAP data landscapes. The Reltio community is a reference point for entity resolution and data-quality patterns that complement SAP master data.", - "personaTags": ["role:developer", "role:architect"] - }, - { - "ID": "66333900-3rd0-0005-0001-000000000001", - "verb": "AI", "shelf": "REFERENCE", "sortOrder": 200, - "title": "Hugging Face", - "url": "https://huggingface.co", - "description": "Hub for open models, datasets, and ML tooling — the de facto registry for the AI ecosystem.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "The open hub for models, datasets, and ML tooling.", - "whyItMatters": "Hugging Face is where most open models and datasets live. Developers building AI on BTP often source or evaluate models here before deploying via SAP AI Core.", - "personaTags": ["role:developer", "role:student", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-0005-0001-000000000002", - "verb": "AI", "shelf": "TOOLS", "sortOrder": 210, - "title": "TabPFN (Prior Labs)", - "url": "https://github.com/PriorLabs/TabPFN", - "description": "Foundation model for tabular data that delivers strong results on small datasets without training.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "A foundation model for tabular data — no training required.", - "whyItMatters": "Most enterprise data is tabular. TabPFN is a notable open model for tabular prediction, relevant to developers exploring ML on structured SAP data.", - "personaTags": ["role:developer", "role:student"] - }, - { - "ID": "66333900-3rd0-0005-0001-000000000003", - "verb": "AI", "shelf": "REFERENCE", "sortOrder": 220, - "title": "Prior Labs Research", - "url": "https://priorlabs.ai/research", - "description": "Research and technical reports behind TabPFN and tabular foundation models.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "The research behind tabular foundation models.", - "whyItMatters": "Prior Labs publishes the papers and reports underpinning TabPFN, including work featured in Nature — a primary source for developers evaluating the approach.", - "personaTags": ["role:developer", "role:student"] - }, - { - "ID": "66333900-3rd0-0005-0001-000000000004", - "verb": "AI", "shelf": "TOOLS", "sortOrder": 230, - "title": "Kaggle", - "url": "https://www.kaggle.com", - "description": "Platform for datasets, notebooks, and ML competitions with a large practitioner community.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Datasets, notebooks, and competitions to sharpen ML skills.", - "whyItMatters": "Kaggle is a practical training ground for data science. Its datasets and notebooks help developers and students build the ML skills they later apply on SAP data.", - "personaTags": ["role:developer", "role:student"] - }, - { - "ID": "66333900-3rd0-0003-0001-000000000001", - "verb": "INTEGRATE", "shelf": "KEEP_CURRENT", "sortOrder": 200, - "title": "n8n Community Forum", - "url": "https://community.n8n.io", - "description": "Official forum for the n8n workflow-automation community — Q&A, templates, and help.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Get help and share n8n automation workflows.", - "whyItMatters": "n8n is a popular open workflow-automation tool used to integrate SAP and non-SAP systems. Its forum is the primary place to find node patterns and troubleshoot flows.", - "personaTags": ["role:developer", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-0003-0001-000000000002", - "verb": "INTEGRATE", "shelf": "KEEP_CURRENT", "sortOrder": 210, - "title": "n8n Discord", - "url": "https://discord.gg/n8n", - "description": "Real-time chat community for n8n users and contributors.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Real-time chat with the n8n community.", - "whyItMatters": "The n8n Discord is where users get quick answers and share in-progress automations — a fast channel when building integrations that touch SAP endpoints.", - "personaTags": ["role:developer"] - }, - { - "ID": "66333900-3rd0-0003-0001-000000000003", - "verb": "INTEGRATE", "shelf": "KEEP_CURRENT", "sortOrder": 220, - "title": "n8n on YouTube", - "url": "https://www.youtube.com/c/n8n-io", - "description": "Official n8n channel — tutorials, feature demos, and automation walkthroughs.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Video tutorials and demos for n8n automation.", - "whyItMatters": "n8n's YouTube channel walks through building automations step by step — a fast way to learn node patterns before wiring up SAP integrations.", - "personaTags": ["role:developer"] - }, - { - "ID": "66333900-3rd0-0003-0001-000000000004", - "verb": "INTEGRATE", "shelf": "TOOLS", "sortOrder": 230, - "title": "n8n (n8n-io/n8n)", - "url": "https://github.com/n8n-io/n8n", - "description": "Source repository for the n8n workflow-automation platform.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "The open-source n8n automation engine on GitHub.", - "whyItMatters": "The n8n repo is the source of truth for the automation engine and its nodes — the place to file issues, read code, and understand how integrations execute.", - "personaTags": ["role:developer", "deployment:cloud"] - }, - { - "ID": "66333900-3rd0-0003-0001-000000000005", - "verb": "INTEGRATE", "shelf": "TOOLS", "sortOrder": 240, - "title": "n8n docs (n8n-io/n8n-docs)", - "url": "https://github.com/n8n-io/n8n-docs", - "description": "Documentation source for n8n — node references and self-hosting guides.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Docs and node references for n8n, open to contributions.", - "whyItMatters": "The n8n-docs repo holds node references and self-hosting guides, and accepts public PRs — useful when documenting a custom SAP integration node.", - "personaTags": ["role:developer"] - }, - { - "ID": "66333900-3rd0-0006-0001-000000000001", - "verb": "CONNECT", "shelf": "KEEP_CURRENT", "sortOrder": 200, - "title": "r/SAP", - "url": "https://www.reddit.com/r/SAP", - "description": "Reddit community discussing SAP products, careers, and day-to-day practice.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Candid SAP discussion from the wider practitioner community.", - "whyItMatters": "r/SAP is an unfiltered view of what SAP practitioners are dealing with — a useful pulse-check beyond official channels.", - "personaTags": ["role:developer"] - }, - { - "ID": "66333900-3rd0-0006-0001-000000000002", - "verb": "CONNECT", "shelf": "KEEP_CURRENT", "sortOrder": 210, - "title": "r/dataengineering", - "url": "https://www.reddit.com/r/dataengineering", - "description": "Reddit community for data engineers — tooling debates, career, and architecture.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Where data engineers debate tools and architecture.", - "whyItMatters": "A high-signal community for data-platform trends and tradeoffs that inform how SAP data fits a broader engineering stack.", - "personaTags": ["role:architect"] - }, - { - "ID": "66333900-3rd0-0006-0001-000000000003", - "verb": "CONNECT", "shelf": "KEEP_CURRENT", "sortOrder": 220, - "title": "r/MachineLearning", - "url": "https://www.reddit.com/r/MachineLearning", - "description": "Reddit community covering ML research, tooling, and applied practice.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "ML research and applied practice, community-curated.", - "whyItMatters": "Keeps developers aware of ML advances they may bring to SAP data — from model families to applied techniques.", - "personaTags": ["role:developer"] - }, - { - "ID": "66333900-3rd0-0006-0001-000000000004", - "verb": "CONNECT", "shelf": "KEEP_CURRENT", "sortOrder": 230, - "title": "r/n8n", - "url": "https://www.reddit.com/r/n8n", - "description": "Reddit community for n8n workflow automation — recipes and troubleshooting.", - "badge": null, "isExternal": true, "isActive": true, - "authoringStatus": "REVIEWED", "personaWeight": 0, - "tagline": "Automation recipes and troubleshooting for n8n.", - "whyItMatters": "A community source for n8n automation recipes, complementing the official forum when building integrations across SAP and other systems.", - "personaTags": ["role:developer"] - } -] diff --git a/docs/developers/architecture/homepage.md b/docs/developers/architecture/homepage.md index b39c3d2c9..713305bea 100644 --- a/docs/developers/architecture/homepage.md +++ b/docs/developers/architecture/homepage.md @@ -148,8 +148,22 @@ Managed via the `LegacyRedirects` CDS entity. The approuter middleware (`approut **Nightly link-health:** The `homepage-link-health` cron job (04:00 daily) sends HEAD requests to every active `HomepageShelves.url` and writes `linkStatus` (`OK` | `SLOW` | `BROKEN`) + `lastChecked` back to the entity. Broken links surface as a red dot on the Shelves tab in the admin UI. Threshold for SLOW is 1500ms (default); timeout is 5000ms per URL; concurrency is 4 with 200ms between requests. +## HomepageShelves seeding (baseline is boot-seeded, not CSV) + +`HomepageShelves` is **admin-managed on HANA** — its production HDI artifacts are structure-only (`.hdbtable` + unique index + change-tracking triggers), with **no `.hdbtabledata`**, so a deploy can never full-replace the table. + +The canonical baseline (SAP-owned entries + curated third-party links such as Prior Labs / Dremio / Reltio / n8n) lives inline in **`srv/lib/homepage/homepage-shelves-defaults.js`** and is seeded idempotently at boot by **`srv/lib/homepage/seed-homepage-shelves.js`** (called from `cds.on('served')` in `srv/server.js`). The seed is **insert-if-missing on `(verb,url)`**: it adds only rows that aren't already present, so admin edits are never overwritten, deleted baseline rows self-heal on the next restart, and it runs safely (non-fatal) in every profile — `cds watch`, unit tests, hybrid, and production. + +This replaced two fragile predecessors (both retired): + +- the `test/data/com.sap.developers.ims-HomepageShelves.csv` seed CSV, which — while it lived in `db/data` — compiled to an `.hdbtabledata` with `include_filter: []` and **full-replaced the table on every deploy whose CSV hash changed**, wiping curated/third-party rows and admin edits (the root cause of the recurring "shelf links disappeared" incidents); and +- the manual `npm run seed:thirdparty` promotion step (DEV/PROD-ambiguous — it followed the CDS binding, so it could silently target the wrong environment). + +To add or curate a baseline link: edit `homepage-shelves-defaults.js`, or use `/admin-ui/#homepage` at runtime (see **Add shelf entry** above). No CSV, no manual seed script. + --- + ## Explainer popovers Issue #759 adds progressive-disclosure explainers to the homepage verb spine, the directory footer, and the verb sub-page link cards. The data model (`VerbDefinitions`, `ShelfDefinitions`, plus three new fields on `HomepageShelves`), build feeds, AI-generation actions, Vue islands, and admin workflow are documented separately to keep this file focused. diff --git a/package.json b/package.json index 85e27981f..22221db2d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev:hybrid": "npm run watch:hybrid & npm run start:approuter", "start:approuter": "cd approuter && NODE_ENV=development npm start", "bind:setup": "node scripts/setup-hybrid-env.js", - "seed:thirdparty": "cds bind --exec -- node scripts/seed-thirdparty.js", + "seed:thirdparty": "echo 'RETIRED: HomepageShelves (incl. third-party links) is now seeded idempotently at boot by srv/lib/homepage/seed-homepage-shelves.js from srv/lib/homepage/homepage-shelves-defaults.js. Edit that module or use /admin-ui/#homepage; no manual seed step needed.' && exit 1", "setup": "npm --prefix hugo-apps install --no-audit --no-fund && npm --prefix app/explore install --no-audit --no-fund && sh scripts/install-git-hooks.sh", "build:cds": "cds build --production", "fetch-tutorials": "tsx scripts/fetch-tutorials.ts --target hugo", diff --git a/scripts/__tests__/seed-thirdparty-data.test.ts b/scripts/__tests__/seed-thirdparty-data.test.ts deleted file mode 100644 index d8530fa7a..000000000 --- a/scripts/__tests__/seed-thirdparty-data.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { KNOWN_TAGS } from '../../srv/lib/homepage/persona-tag-validator.js'; - -const KNOWN = new Set(KNOWN_TAGS); -const VERBS = new Set(['LEARN', 'BUILD', 'INTEGRATE', 'MODEL', 'OPERATE', 'AI', 'CONNECT']); -const SHELVES = new Set(['START_HERE', 'REFERENCE', 'TOOLS', 'KEEP_CURRENT']); - -const jsonUrl = new URL('../../db/data/staging/homepage-thirdparty.json', import.meta.url); -// Seed CSV moved db/data/ → test/data/ in #1673 (stops deploy wiping admin-editable columns). -const csvUrl = new URL('../../test/data/com.sap.developers.ims-HomepageShelves.csv', import.meta.url); - -const raw = JSON.parse(readFileSync(fileURLToPath(jsonUrl), 'utf-8')); -const rows = raw.filter((r) => !r._comment); - -const csvText = readFileSync(fileURLToPath(csvUrl), 'utf-8'); -const csvLines = csvText.split(/\r?\n/).filter((l) => l.trim()); -const csvHeader = csvLines[0].split(';'); -const idIdx = csvHeader.indexOf('ID'); -const verbIdx = csvHeader.indexOf('verb'); -const urlIdx = csvHeader.indexOf('url'); -const csvIds = new Set(csvLines.slice(1).map((l) => l.split(';')[idIdx])); -const csvVerbUrls = new Set(csvLines.slice(1).map((l) => { - const c = l.split(';'); - return `${c[verbIdx]}|${c[urlIdx]}`; -})); - -describe('homepage-thirdparty staging data', () => { - it('parses to a non-empty array of content rows', () => { - expect(Array.isArray(raw)).toBe(true); - expect(rows.length).toBe(23); - }); - - it('every row has required fields with correct fixed values', () => { - const ALLOWED_BADGES = new Set([null, 'NEW', 'UPDATED', 'HIDDEN_GEM', 'THIRD_PARTY']); - for (const r of rows) { - expect(typeof r.ID).toBe('string'); - expect(VERBS.has(r.verb)).toBe(true); - expect(SHELVES.has(r.shelf)).toBe(true); - expect(Number.isInteger(r.sortOrder)).toBe(true); - expect(r.title.length).toBeGreaterThan(0); - expect(r.title.length).toBeLessThanOrEqual(120); - expect(r.description.length).toBeLessThanOrEqual(280); - expect(r.tagline.length).toBeLessThanOrEqual(140); - expect(r.whyItMatters.length).toBeLessThanOrEqual(800); - // THIRD_PARTY badge intentionally retired — it lost meaning at scale. - // Badge is now null for the ecosystem links; RPT-1 rows carry NEW. - expect(ALLOWED_BADGES.has(r.badge)).toBe(true); - expect(r.badge).not.toBe('THIRD_PARTY'); - expect(r.isExternal).toBe(true); - expect(r.isActive).toBe(true); - expect(r.authoringStatus).toBe('REVIEWED'); - expect(r.personaWeight).toBe(0); - } - }); - - it('every url is absolute https', () => { - for (const r of rows) { - expect(r.url.startsWith('https://')).toBe(true); - } - }); - - it('every persona tag is in KNOWN_TAGS', () => { - for (const r of rows) { - expect(Array.isArray(r.personaTags)).toBe(true); - for (const t of r.personaTags) { - expect(KNOWN.has(t)).toBe(true); - } - } - }); - - it('(verb,url) pairs are unique within the file', () => { - const seen = new Set(); - for (const r of rows) { - const key = `${r.verb}|${r.url}`; - expect(seen.has(key)).toBe(false); - seen.add(key); - } - }); - - it('does not collide with existing canonical CSV on ID or (verb,url)', () => { - for (const r of rows) { - expect(csvIds.has(r.ID)).toBe(false); - expect(csvVerbUrls.has(`${r.verb}|${r.url}`)).toBe(false); - } - }); -}); diff --git a/scripts/__tests__/seed-thirdparty-loader.test.ts b/scripts/__tests__/seed-thirdparty-loader.test.ts deleted file mode 100644 index 5e1976582..000000000 --- a/scripts/__tests__/seed-thirdparty-loader.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import cds from '@sap/cds'; -import { seedThirdParty } from '../seed-thirdparty.js'; - -let db; -let HomepageShelves; - -beforeAll(async () => { - const model = await cds.load('*'); - db = await cds.deploy(model).to('sqlite::memory:'); - ({ HomepageShelves } = cds.entities('com.sap.developers.ims')); -}, 60000); - -describe('seedThirdParty', () => { - it('first run inserts all rows', async () => { - const res = await seedThirdParty(db); - expect(res.inserted).toBe(23); - expect(res.updated).toBe(0); - const staged = await db.run(SELECT.from(HomepageShelves).columns('ID').where( - `ID like '66333900-3rd0-%' or ID like '66333900-rpt1-%'` - )); - expect(staged.length).toBe(23); - }); - - it('second run updates in place, inserts nothing (idempotent)', async () => { - const res = await seedThirdParty(db); - expect(res.inserted).toBe(0); - expect(res.updated).toBe(23); - const staged = await db.run(SELECT.from(HomepageShelves).columns('ID').where( - `ID like '66333900-3rd0-%' or ID like '66333900-rpt1-%'` - )); - expect(staged.length).toBe(23); - }); - - it('persists personaTags as an array and clears the THIRD_PARTY badge', async () => { - const vercel = await db.run(SELECT.one.from(HomepageShelves).where({ verb: 'BUILD', url: 'https://vercel.com' })); - expect(vercel).toBeTruthy(); - expect(vercel.personaTags).toContain('role:developer'); - expect(vercel.badge).toBeNull(); - }); - - it('marks the RPT-1 rows with the NEW badge', async () => { - const rpt = await db.run(SELECT.one.from(HomepageShelves).where({ verb: 'AI', url: 'https://rpt.cloud.sap' })); - expect(rpt).toBeTruthy(); - expect(rpt.title).toBe('SAP RPT-1 Playground'); - expect(rpt.badge).toBe('NEW'); - }); -}); diff --git a/scripts/seed-thirdparty.js b/scripts/seed-thirdparty.js deleted file mode 100644 index d5e8ef6d9..000000000 --- a/scripts/seed-thirdparty.js +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env node -// Idempotent loader for homepage third-party content. -// Upserts db/data/staging/homepage-thirdparty.json into HomepageShelves, -// keyed on (verb, url). Run against DEV: `npm run seed:thirdparty`. -// PROD promotion is manual — see the file's _comment and the spec -// (docs/superpowers/specs/2026-07-30-homepage-thirdparty-content-design.md). -import cds from '@sap/cds'; -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { KNOWN_TAGS } from '../srv/lib/homepage/persona-tag-validator.js'; - -const KNOWN = new Set(KNOWN_TAGS); -const DATA_URL = new URL('../db/data/staging/homepage-thirdparty.json', import.meta.url); - -function loadRows() { - const raw = JSON.parse(readFileSync(fileURLToPath(DATA_URL), 'utf-8')); - return raw.filter((r) => !r._comment); -} - -function validate(rows) { - const errors = []; - const seen = new Set(); - for (const r of rows) { - if (!r.verb || !r.url) errors.push(`row ${r.ID || '?'}: missing verb/url`); - if (typeof r.url !== 'string' || !r.url.startsWith('https://')) { - errors.push(`row ${r.ID}: url must be absolute https`); - } - const key = `${r.verb}|${r.url}`; - if (seen.has(key)) errors.push(`duplicate (verb,url): ${key}`); - seen.add(key); - for (const t of r.personaTags || []) { - if (!KNOWN.has(t)) errors.push(`row ${r.ID}: unknown personaTag "${t}"`); - } - } - if (errors.length) { - throw new Error(`seed-thirdparty validation failed:\n ${errors.join('\n ')}`); - } -} - -const NAMESPACE = 'com.sap.developers.ims'; - -export async function seedThirdParty(dbOverride) { - const db = dbOverride ?? await cds.connect.to('db'); - // Reflect the entity via cds.linked so CQL is type-aware and serializes the - // `personaTags` array to JSON for the HANA NCLOB column. `cds.entities` is a - // getter that is undefined in a standalone script (only set after cds.serve), - // and a fully-qualified string name is NOT type-aware (fails on HANA with - // "Wrong input for LOB type"). cds.linked(csn) works in every context. - const linked = cds.linked(cds.model ?? await cds.load('*')); - const { HomepageShelves } = linked.entities(NAMESPACE); - const rows = loadRows(); - validate(rows); - - let inserted = 0; - let updated = 0; - for (const r of rows) { - const existing = await db.run(SELECT.one.from(HomepageShelves).columns('ID').where({ verb: r.verb, url: r.url })); - if (existing) { - const { ID, ...patch } = r; // keep existing ID on update - await db.run(UPDATE(HomepageShelves).set(patch).where({ ID: existing.ID })); - updated++; - } else { - await db.run(INSERT.into(HomepageShelves).entries(r)); - inserted++; - } - } - return { inserted, updated }; -} - -// CLI entry -if (fileURLToPath(import.meta.url) === process.argv[1]) { - seedThirdParty() - .then(({ inserted, updated }) => { - console.log(`seed-thirdparty: ${inserted} inserted, ${updated} updated`); - process.exit(0); - }) - .catch((err) => { - console.error(err.message || err); - process.exit(1); - }); -} diff --git a/srv/lib/homepage/homepage-shelves-defaults.js b/srv/lib/homepage/homepage-shelves-defaults.js new file mode 100644 index 000000000..fbc824f93 --- /dev/null +++ b/srv/lib/homepage/homepage-shelves-defaults.js @@ -0,0 +1,1297 @@ +// srv/lib/homepage/homepage-shelves-defaults.js +// +// CANONICAL baseline for the HomepageShelves table (verb-page shelf entries + +// footer links). Single source of truth, seeded idempotently at boot by +// srv/lib/homepage/seed-homepage-shelves.js (called from cds.on('served')). +// +// Replaces the former test/data seed CSV + the manual db/data/staging +// third-party JSON + `npm run seed:thirdparty`. HomepageShelves is +// admin-managed on HANA (no .hdbtabledata ships in --production), so these +// rows are inserted ONLY when their (verb,url) is missing — admin edits to +// existing rows are never overwritten, and a deploy can never full-replace +// the table (that was the pre-#1404c4c4 data-loss bug). +// +// To add/curate a baseline link: edit this array (or use /admin-ui/#homepage +// at runtime). Rows: 97 across 7 verbs — {"LEARN":9,"BUILD":14,"INTEGRATE":14,"OPERATE":9,"AI":16,"CONNECT":16,"MODEL":19} +// +// GENERATED once from the retired CSV + staging JSON; now hand-maintained. + +export const HOMEPAGE_SHELVES_DEFAULTS = [ + { + "ID": "66333900-0001-0001-0001-000000000001", + "verb": "LEARN", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "Tutorial Navigator", + "url": "/tutorial-navigator/", + "isExternal": false, + "isActive": true, + "description": "Browse and filter 1 400+ hands-on tutorials across all SAP topics" + }, + { + "ID": "66333900-0001-0001-0001-000000000002", + "verb": "LEARN", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "SAP Learning Journeys", + "url": "https://learning.sap.com/learning-journeys", + "isExternal": true, + "isActive": true, + "description": "Curated multi-course paths toward certifications and role mastery" + }, + { + "ID": "66333900-0001-0001-0001-000000000003", + "verb": "LEARN", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "New to Cloud SAP?", + "url": "/missions/", + "isExternal": false, + "isActive": true, + "description": "Guided mission paths for developers moving from on-prem to cloud and AI" + }, + { + "ID": "66333900-0001-0001-0001-000000000004", + "verb": "LEARN", + "shelf": "REFERENCE", + "sortOrder": 40, + "title": "learning.sap.com", + "url": "https://learning.sap.com", + "isExternal": true, + "isActive": true, + "description": "Free and paid learning journeys, self-paced courses, and certification prep" + }, + { + "ID": "66333900-0001-0001-0001-000000000005", + "verb": "LEARN", + "shelf": "REFERENCE", + "sortOrder": 50, + "title": "SAP Help Portal", + "url": "https://help.sap.com", + "isExternal": true, + "isActive": true, + "description": "Centralized product documentation for all SAP products" + }, + { + "ID": "66333900-0001-0001-0001-000000000006", + "verb": "LEARN", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "Certifications", + "url": "https://www.sap.com/training-certification.html", + "isExternal": true, + "isActive": true, + "description": "Official SAP certification paths and instructor-led training" + }, + { + "ID": "66333900-0001-0001-0001-000000000007", + "verb": "LEARN", + "shelf": "TOOLS", + "sortOrder": 70, + "title": "BTP Free Tier Signup", + "url": "https://cockpit.btp.cloud.sap", + "isExternal": true, + "isActive": true, + "description": "Start building on SAP BTP with a free tier account — no credit card required" + }, + { + "ID": "66333900-0001-0001-0001-000000000008", + "verb": "LEARN", + "shelf": "TOOLS", + "sortOrder": 80, + "title": "SAP-samples on GitHub", + "url": "https://github.com/SAP-samples", + "isExternal": true, + "isActive": true, + "description": "Hundreds of sample apps, tutorials, and workshop repos from SAP" + }, + { + "ID": "66333900-0001-0001-0001-000000000009", + "verb": "LEARN", + "shelf": "KEEP_CURRENT", + "sortOrder": 90, + "title": "SAP Developer News", + "url": "https://youtube.com/@sapdevs", + "isExternal": true, + "isActive": true, + "description": "Weekly Friday show covering the latest developer news, tutorials, and events" + }, + { + "ID": "66333900-0001-0002-0001-000000000001", + "verb": "BUILD", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "SAP CAP", + "url": "https://cap.cloud.sap", + "isExternal": true, + "isActive": true, + "description": "Official docs for the Cloud Application Programming Model — Node.js + Java" + }, + { + "ID": "66333900-0001-0002-0001-000000000002", + "verb": "BUILD", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "ABAP Cloud + RAP", + "url": "https://help.sap.com/docs/abap-cloud", + "isExternal": true, + "isActive": true, + "description": "Clean-core ABAP development with RAP, ADT, and the BTP ABAP environment" + }, + { + "ID": "66333900-0001-0002-0001-000000000003", + "verb": "BUILD", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "Fiori / UI5", + "url": "https://ui5.sap.com", + "isExternal": true, + "isActive": true, + "description": "SAP UI5 Demo Kit — API reference, samples, and interactive demos" + }, + { + "ID": "66333900-0001-0002-0001-000000000004", + "verb": "BUILD", + "shelf": "START_HERE", + "sortOrder": 40, + "title": "SAP Build", + "url": "https://www.sap.com/products/technology-platform/build.html", + "isExternal": true, + "isActive": true, + "description": "Low-code app, automation, and website suite on BTP for faster delivery" + }, + { + "ID": "66333900-0001-0002-0001-000000000005", + "verb": "BUILD", + "shelf": "START_HERE", + "sortOrder": 50, + "title": "SAP Build Code", + "url": "https://www.sap.com/products/technology-platform/build-code.html", + "isExternal": true, + "isActive": true, + "description": "AI-assisted pro-code development tooling on BTP with Joule co-pilot" + }, + { + "ID": "66333900-0001-0002-0001-000000000006", + "verb": "BUILD", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "SAP Cloud SDK", + "url": "https://sap.github.io/cloud-sdk/", + "isExternal": true, + "isActive": true, + "description": "Java + JavaScript SDKs for consuming SAP APIs from any cloud app" + }, + { + "ID": "66333900-0001-0002-0001-000000000007", + "verb": "BUILD", + "shelf": "REFERENCE", + "sortOrder": 70, + "title": "Fiori Design System", + "url": "https://www.sap.com/design-system/", + "isExternal": true, + "isActive": true, + "description": "SAP Fiori design guidelines for web, mobile, and Build apps" + }, + { + "ID": "66333900-0001-0002-0001-000000000008", + "verb": "BUILD", + "shelf": "REFERENCE", + "sortOrder": 80, + "title": "Business Application Studio", + "url": "https://help.sap.com/docs/SAP_BUSINESS_APPLICATION_STUDIO", + "isExternal": true, + "isActive": true, + "description": "Official docs for the cloud IDE purpose-built for SAP development" + }, + { + "ID": "66333900-0001-0002-0001-000000000009", + "verb": "BUILD", + "shelf": "TOOLS", + "sortOrder": 90, + "title": "SAP Development Tools", + "url": "https://tools.hana.ondemand.com", + "isExternal": true, + "isActive": true, + "description": "Download portal for Eclipse plugins, CLIs, HANA client, and SDKs" + }, + { + "ID": "66333900-0001-0002-0001-000000000010", + "verb": "BUILD", + "shelf": "TOOLS", + "sortOrder": 100, + "title": "UI5 Web Components for React", + "url": "https://sap.github.io/ui5-webcomponents-react/", + "isExternal": true, + "isActive": true, + "description": "React wrappers for UI5 web components — enterprise-grade UI in React" + }, + { + "ID": "66333900-0001-0002-0001-000000000011", + "verb": "BUILD", + "shelf": "KEEP_CURRENT", + "sortOrder": 110, + "title": "SAP Tech Bytes", + "url": "https://youtube.com/@sapdevs", + "isExternal": true, + "isActive": true, + "description": "Short-form, code-focused SAP Developers YouTube videos" + }, + { + "ID": "66333900-0001-0002-0001-000000000012", + "verb": "BUILD", + "shelf": "KEEP_CURRENT", + "sortOrder": 120, + "title": "CAP Community Blogs", + "url": "https://community.sap.com/t5/all-sap-community-blogs/ct-p/all-blogs", + "isExternal": true, + "isActive": true, + "description": "Latest community articles on CAP, ABAP, Fiori, and BTP development" + }, + { + "ID": "66333900-0001-0002-0001-000000000013", + "verb": "BUILD", + "shelf": "KEEP_CURRENT", + "sortOrder": 130, + "title": "CodeJams", + "url": "/tutorial-navigator/", + "isExternal": false, + "isActive": true, + "description": "Hands-on in-person and virtual workshops delivered by SAP experts" + }, + { + "ID": "66333900-0001-0003-0001-000000000001", + "verb": "INTEGRATE", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "SAP Business Accelerator Hub", + "url": "https://api.sap.com", + "isExternal": true, + "isActive": true, + "description": "Browse and test SAP APIs across S/4HANA, SuccessFactors, Ariba, and more" + }, + { + "ID": "66333900-0001-0003-0001-000000000002", + "verb": "INTEGRATE", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "SAP Integration Suite", + "url": "https://help.sap.com/docs/integration-suite", + "isExternal": true, + "isActive": true, + "description": "Docs for Cloud Integration, API Management, and Event Mesh on BTP" + }, + { + "ID": "66333900-0001-0003-0001-000000000003", + "verb": "INTEGRATE", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "Your First Integration Flow", + "url": "/tutorial-navigator/", + "isExternal": false, + "isActive": true, + "description": "Step-by-step tutorials for building your first integration on SAP BTP" + }, + { + "ID": "66333900-0001-0003-0001-000000000004", + "verb": "INTEGRATE", + "shelf": "REFERENCE", + "sortOrder": 40, + "title": "SAP Event Mesh", + "url": "https://help.sap.com/docs/event-mesh", + "isExternal": true, + "isActive": true, + "description": "Managed message broker for event-driven architectures on BTP" + }, + { + "ID": "66333900-0001-0003-0001-000000000005", + "verb": "INTEGRATE", + "shelf": "REFERENCE", + "sortOrder": 50, + "title": "SAP Destination Service", + "url": "https://help.sap.com/docs/destination-service", + "isExternal": true, + "isActive": true, + "description": "BTP service that brokers credentials to external endpoints securely" + }, + { + "ID": "66333900-0001-0003-0001-000000000006", + "verb": "INTEGRATE", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "Open Resource Discovery", + "url": "https://open-resource-discovery.org", + "isExternal": true, + "isActive": true, + "description": "Open protocol for publishing and discovering application + service metadata" + }, + { + "ID": "66333900-0001-0003-0001-000000000007", + "verb": "INTEGRATE", + "shelf": "REFERENCE", + "sortOrder": 70, + "title": "SAP OData Vocabularies", + "url": "https://sap.github.io/odata-vocabularies/", + "isExternal": true, + "isActive": true, + "description": "OData annotation term definitions across SAP business domains" + }, + { + "ID": "66333900-0001-0003-0001-000000000008", + "verb": "INTEGRATE", + "shelf": "TOOLS", + "sortOrder": 80, + "title": "Project Piper", + "url": "https://www.project-piper.io/", + "isExternal": true, + "isActive": true, + "description": "Pre-built CI/CD pipelines and shared library for SAP application delivery" + }, + { + "ID": "66333900-0001-0003-0001-000000000009", + "verb": "INTEGRATE", + "shelf": "TOOLS", + "sortOrder": 90, + "title": "Integration Samples", + "url": "https://github.com/SAP-samples", + "isExternal": true, + "isActive": true, + "description": "Integration flow samples, API patterns, and event-driven architecture demos" + }, + { + "ID": "66333900-0001-0004-0001-000000000001", + "verb": "OPERATE", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "SAP BTP Cockpit", + "url": "https://cockpit.btp.cloud.sap", + "isExternal": true, + "isActive": true, + "description": "Browser console for managing BTP global accounts, subaccounts, and spaces" + }, + { + "ID": "66333900-0001-0004-0001-000000000002", + "verb": "OPERATE", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "BTP CLI", + "url": "https://help.sap.com/docs/btp-cli", + "isExternal": true, + "isActive": true, + "description": "Command-line reference for the `btp` tool — automate BTP account management" + }, + { + "ID": "66333900-0001-0004-0001-000000000003", + "verb": "OPERATE", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "BTP Getting Started", + "url": "https://help.sap.com/docs/btp/sap-business-technology-platform/getting-started", + "isExternal": true, + "isActive": true, + "description": "Onboarding entry point and checklist for SAP Business Technology Platform" + }, + { + "ID": "66333900-0001-0004-0001-000000000004", + "verb": "OPERATE", + "shelf": "REFERENCE", + "sortOrder": 40, + "title": "SAP BTP Documentation", + "url": "https://help.sap.com/docs/btp", + "isExternal": true, + "isActive": true, + "description": "Canonical reference docs for SAP Business Technology Platform" + }, + { + "ID": "66333900-0001-0004-0001-000000000005", + "verb": "OPERATE", + "shelf": "REFERENCE", + "sortOrder": 50, + "title": "SAP Discovery Center", + "url": "https://discovery-center.cloud.sap", + "isExternal": true, + "isActive": true, + "description": "BTP service catalog, guided missions, estimator, and pricing" + }, + { + "ID": "66333900-0001-0004-0001-000000000006", + "verb": "OPERATE", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "Kyma", + "url": "https://kyma-project.io", + "isExternal": true, + "isActive": true, + "description": "Open-source Kubernetes platform for cloud-native BTP extensions" + }, + { + "ID": "66333900-0001-0004-0001-000000000007", + "verb": "OPERATE", + "shelf": "REFERENCE", + "sortOrder": 70, + "title": "SAP HANA Cloud", + "url": "https://help.sap.com/docs/SAP_HANA_CLOUD", + "isExternal": true, + "isActive": true, + "description": "Canonical docs for HANA Cloud in-memory database with vector engine" + }, + { + "ID": "66333900-0001-0004-0001-000000000008", + "verb": "OPERATE", + "shelf": "TOOLS", + "sortOrder": 80, + "title": "Business Application Studio", + "url": "https://help.sap.com/docs/SAP_BUSINESS_APPLICATION_STUDIO", + "isExternal": true, + "isActive": true, + "description": "Cloud IDE purpose-built for CAP, Fiori, mobile, and full-stack BTP development" + }, + { + "ID": "66333900-0001-0004-0001-000000000009", + "verb": "OPERATE", + "shelf": "TOOLS", + "sortOrder": 90, + "title": "SAP Development Tools", + "url": "https://tools.hana.ondemand.com", + "isExternal": true, + "isActive": true, + "description": "Download Eclipse plugins, CLIs, HANA client, and SDKs" + }, + { + "ID": "66333900-0001-0005-0001-000000000001", + "verb": "AI", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "BTP AI Best Practices", + "url": "https://btp-ai-bp.docs.sap/", + "isExternal": true, + "isActive": true, + "description": "Curated guides and architecture patterns for building AI apps on BTP" + }, + { + "ID": "66333900-0001-0005-0001-000000000002", + "verb": "AI", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "AI Skills Library", + "url": "https://skills.cloud.sap/", + "isExternal": true, + "isActive": true, + "description": "Searchable catalog of certified AI skills built on SAP BTP" + }, + { + "ID": "66333900-0001-0005-0001-000000000009", + "verb": "AI", + "shelf": "START_HERE", + "sortOrder": 25, + "title": "Joule Studio", + "url": "https://help.sap.com/docs/joule-studio", + "isExternal": true, + "isActive": true, + "description": "Low-code authoring environment for custom Joule skills, prompts, and tool integrations — next-generation edition (Q3 2026)", + "badge": "NEW" + }, + { + "ID": "66333900-0001-0005-0001-000000000003", + "verb": "AI", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "Your First Joule Extension", + "url": "/tutorial-navigator/", + "isExternal": false, + "isActive": true, + "description": "Tutorial series for building custom Joule extensions with the AI SDK" + }, + { + "ID": "66333900-0001-0005-0001-000000000004", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 40, + "title": "SAP Joule", + "url": "https://help.sap.com/docs/joule", + "isExternal": true, + "isActive": true, + "description": "Docs for SAP's generative-AI copilot embedded across SAP products" + }, + { + "ID": "66333900-0001-0005-0001-000000000010", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 45, + "title": "Joule Studio (Classic)", + "url": "https://help.sap.com/docs/joule-studio#classic", + "isExternal": true, + "isActive": true, + "description": "Classic Joule Studio edition — GA since December 2025 — for teams already shipping skills today" + }, + { + "ID": "66333900-0001-0005-0001-000000000005", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 50, + "title": "SAP AI Core", + "url": "https://help.sap.com/docs/sap-ai-core", + "isExternal": true, + "isActive": true, + "description": "Runtime for training and serving ML + GenAI models on BTP" + }, + { + "ID": "66333900-0001-0005-0001-000000000006", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "SAP AI Launchpad", + "url": "https://help.sap.com/docs/sap-ai-launchpad", + "isExternal": true, + "isActive": true, + "description": "Web UI to manage AI scenarios, models, and deployments on BTP" + }, + { + "ID": "66333900-0001-0005-0001-000000000007", + "verb": "AI", + "shelf": "TOOLS", + "sortOrder": 70, + "title": "AI4U Use Case Repository", + "url": "https://ai4u-website.cfapps.eu10-004.hana.ondemand.com", + "isExternal": true, + "isActive": true, + "description": "50+ real-world AI projects built on SAP BTP — searchable by scenario" + }, + { + "ID": "66333900-0001-0005-0001-000000000008", + "verb": "AI", + "shelf": "TOOLS", + "sortOrder": 80, + "title": "RAG on HANA Cookbook", + "url": "https://github.com/SAP-samples", + "isExternal": true, + "isActive": true, + "description": "Retrieval-augmented generation patterns using HANA Cloud vector engine" + }, + { + "ID": "66333900-0001-0006-0001-000000000001", + "verb": "CONNECT", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "SAP Community", + "url": "https://community.sap.com", + "isExternal": true, + "isActive": true, + "description": "Networking, Q&A, blogs, and groups for all SAP developer roles" + }, + { + "ID": "66333900-0001-0006-0001-000000000002", + "verb": "CONNECT", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "SAP Developers YouTube", + "url": "https://youtube.com/@sapdevs", + "isExternal": true, + "isActive": true, + "description": "Tutorial videos, Tech Bytes, Developer News, and live streams" + }, + { + "ID": "66333900-0001-0006-0001-000000000003", + "verb": "CONNECT", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "Devtoberfest", + "url": "https://community.sap.com/t5/devtoberfest/gh-p/Devtoberfest", + "isExternal": true, + "isActive": true, + "description": "Annual open developer celebration with sessions, challenges, and prizes" + }, + { + "ID": "66333900-0001-0006-0001-000000000004", + "verb": "CONNECT", + "shelf": "REFERENCE", + "sortOrder": 40, + "title": "SAP News Center", + "url": "https://news.sap.com", + "isExternal": true, + "isActive": true, + "description": "Official corporate news, customer stories, and product announcements" + }, + { + "ID": "66333900-0001-0006-0001-000000000005", + "verb": "CONNECT", + "shelf": "REFERENCE", + "sortOrder": 50, + "title": "Community Blogs", + "url": "https://community.sap.com/t5/all-sap-community-blogs/ct-p/all-blogs", + "isExternal": true, + "isActive": true, + "description": "Aggregated blog stream from the SAP Community" + }, + { + "ID": "66333900-0001-0006-0001-000000000006", + "verb": "CONNECT", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "Developer Advocates", + "url": "/developer-advocates/", + "isExternal": false, + "isActive": true, + "description": "Meet the SAP Developer Advocate team and find their content" + }, + { + "ID": "66333900-0001-0006-0001-000000000007", + "verb": "CONNECT", + "shelf": "TOOLS", + "sortOrder": 70, + "title": "SAP on GitHub", + "url": "https://github.com/SAP", + "isExternal": true, + "isActive": true, + "description": "SAP's open-source projects and repositories on GitHub" + }, + { + "ID": "66333900-0001-0006-0001-000000000008", + "verb": "CONNECT", + "shelf": "TOOLS", + "sortOrder": 80, + "title": "SAP-samples on GitHub", + "url": "https://github.com/SAP-samples", + "isExternal": true, + "isActive": true, + "description": "Sample code, tutorial repos, and workshop materials from SAP" + }, + { + "ID": "66333900-0001-0006-0001-000000000009", + "verb": "CONNECT", + "shelf": "TOOLS", + "sortOrder": 90, + "title": "SAP-docs on GitHub", + "url": "https://github.com/SAP-docs", + "isExternal": true, + "isActive": true, + "description": "Markdown sources for BTP/UI5/ABAP docs — public PRs welcome", + "badge": "HIDDEN_GEM" + }, + { + "ID": "66333900-0001-0006-0001-000000000010", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 100, + "title": "SAP TechEd", + "url": "https://www.sap.com/about/events/sap-teched.html", + "isExternal": true, + "isActive": true, + "description": "Annual developer conference with sessions, workshops, and hands-on labs" + }, + { + "ID": "66333900-0001-0006-0001-000000000011", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 110, + "title": "ASUG", + "url": "https://www.asug.com", + "isExternal": true, + "isActive": true, + "description": "North America's largest SAP user community — events and networking", + "badge": "THIRD_PARTY" + }, + { + "ID": "66333900-0001-0006-0001-000000000012", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 120, + "title": "SAPinsider", + "url": "https://sapinsider.org", + "isExternal": true, + "isActive": true, + "description": "Independent SAP professional community publication and events", + "badge": "THIRD_PARTY" + }, + { + "ID": "66333900-1029-1001-0001-000000000001", + "verb": "MODEL", + "shelf": "START_HERE", + "sortOrder": 10, + "title": "SAP HANA Cloud", + "url": "https://help.sap.com/docs/hana-cloud", + "isExternal": true, + "isActive": true, + "description": "Managed in-memory database with columnar analytics, spatial, and graph capabilities" + }, + { + "ID": "66333900-1029-1001-0001-000000000002", + "verb": "MODEL", + "shelf": "START_HERE", + "sortOrder": 20, + "title": "SAP Datasphere", + "url": "https://help.sap.com/docs/SAP_DATASPHERE", + "isExternal": true, + "isActive": true, + "description": "Business data fabric — model, federate, and govern data across SAP and third-party sources" + }, + { + "ID": "66333900-1029-1001-0001-000000000003", + "verb": "MODEL", + "shelf": "START_HERE", + "sortOrder": 30, + "title": "SAP Business Data Cloud", + "url": "https://www.sap.com/products/data-cloud/business-data-cloud.html", + "isExternal": true, + "isActive": true, + "description": "Unified data platform that combines SAP + Databricks for AI-ready business data", + "badge": "NEW" + }, + { + "ID": "66333900-1029-1002-0001-000000000001", + "verb": "MODEL", + "shelf": "REFERENCE", + "sortOrder": 40, + "title": "HANA Cloud Modeling Guide", + "url": "https://help.sap.com/docs/hana-cloud/sap-hana-cloud-modeling-guide/sap-hana-cloud-modeling-guide", + "isExternal": true, + "isActive": true, + "description": "Reference for calculation views, HDI containers, and semantic modeling in HANA Cloud" + }, + { + "ID": "66333900-1029-1002-0001-000000000002", + "verb": "MODEL", + "shelf": "REFERENCE", + "sortOrder": 50, + "title": "Datasphere Modeling", + "url": "https://help.sap.com/docs/SAP_DATASPHERE/9f804b8efa8043539289f42f372c4862/459f2ecb37fd4c0299edac35e1670ac6.html", + "isExternal": true, + "isActive": true, + "description": "Build semantic views, data flows, and analytic models in Datasphere spaces" + }, + { + "ID": "66333900-1029-1002-0001-000000000003", + "verb": "MODEL", + "shelf": "REFERENCE", + "sortOrder": 60, + "title": "SAP Analytics Cloud", + "url": "https://help.sap.com/docs/SAP_ANALYTICS_CLOUD", + "isExternal": true, + "isActive": true, + "description": "Cloud analytics — stories, dashboards, planning, and predictive on top of your data models" + }, + { + "ID": "66333900-1029-1003-0001-000000000001", + "verb": "MODEL", + "shelf": "TOOLS", + "sortOrder": 70, + "title": "SAP HANA Database Explorer", + "url": "https://help.sap.com/docs/hana-cloud-database/sap-hana-database-explorer/sap-hana-database-explorer", + "isExternal": true, + "isActive": true, + "description": "Web-based SQL console, catalog browser, and modeling tool for HANA Cloud" + }, + { + "ID": "66333900-1029-1003-0001-000000000002", + "verb": "MODEL", + "shelf": "TOOLS", + "sortOrder": 80, + "title": "hana-cli", + "url": "https://www.npmjs.com/package/hana-cli", + "isExternal": true, + "isActive": true, + "description": "Command-line toolkit for HANA — SQL, table inspection, data profiling, and imports" + }, + { + "ID": "66333900-1029-1003-0001-000000000003", + "verb": "MODEL", + "shelf": "TOOLS", + "sortOrder": 90, + "title": "Data Model Samples", + "url": "https://github.com/SAP-samples?q=hana&type=all", + "isExternal": true, + "isActive": true, + "description": "Canonical HANA, Datasphere, and analytics data-model examples on SAP-samples" + }, + { + "ID": "66333900-1029-1004-0001-000000000001", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 100, + "title": "HANA Cloud What's New", + "url": "https://help.sap.com/docs/hana-cloud/sap-hana-cloud-what-s-new/sap-hana-cloud-what-s-new", + "isExternal": true, + "isActive": true, + "description": "Release notes for HANA Cloud — new features and quarterly capability additions" + }, + { + "ID": "66333900-1029-1004-0001-000000000002", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 110, + "title": "Datasphere Roadmap", + "url": "https://roadmaps.sap.com/board?PRODUCT=73555000100800002141", + "isExternal": true, + "isActive": true, + "description": "Public roadmap for SAP Datasphere — planned innovations and delivery timelines" + }, + { + "ID": "66333900-1029-1004-0001-000000000003", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 120, + "title": "Data & Analytics Community", + "url": "https://community.sap.com/t5/technology/ct-p/technology-blogs", + "isExternal": true, + "isActive": true, + "description": "SAP Community blogs and Q&A for HANA, Datasphere, BDC, and SAC practitioners" + }, + { + "ID": "66333900-rpt1-0005-0001-000000000001", + "verb": "AI", + "shelf": "START_HERE", + "sortOrder": 26, + "title": "SAP RPT-1 Playground", + "url": "https://rpt.cloud.sap", + "description": "Try SAP's RPT-1 tabular foundation model in the browser — bring your own data or use SAP-provided example datasets.", + "badge": "NEW", + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Predict on your tables with RPT-1 — no training, right in the browser.", + "whyItMatters": "RPT-1 is SAP's semantics-aware foundation model for tabular data. The playground lets you run classification and regression on your own tables or SAP sample datasets without any setup — the fastest way to see what the model does.", + "personaTags": [ + "role:developer", + "role:student", + "deployment:cloud" + ] + }, + { + "ID": "66333900-rpt1-0005-0001-000000000002", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 35, + "title": "SAP RPT-1 on Hugging Face", + "url": "https://huggingface.co/SAP/sap-rpt-1-oss", + "description": "Open-source release of SAP RPT-1 — model card, weights, and usage examples for the tabular foundation model.", + "badge": "NEW", + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "The open-source RPT-1 model card, weights, and examples.", + "whyItMatters": "RPT-1 is published open-source on Hugging Face as a semantics-aware tabular in-context learner for classification and regression. The model card is the reference for developers integrating it into their own pipelines.", + "personaTags": [ + "role:developer", + "role:student", + "deployment:cloud" + ] + }, + { + "ID": "66333900-rpt1-1029-0001-000000000001", + "verb": "MODEL", + "shelf": "REFERENCE", + "sortOrder": 65, + "title": "SAP RPT-1", + "url": "https://rpt.cloud.sap", + "description": "SAP's relational/tabular foundation model — predict directly on structured data without task-specific training.", + "badge": "NEW", + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "A foundation model that predicts directly on your data models.", + "whyItMatters": "RPT-1 brings foundation-model prediction to structured, tabular data — the shape of most SAP business data. For teams modeling data in HANA Cloud or Datasphere, it is a fast path to classification and regression without building a bespoke ML pipeline.", + "personaTags": [ + "role:developer", + "role:architect", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-0002-0001-000000000001", + "verb": "BUILD", + "shelf": "TOOLS", + "sortOrder": 200, + "title": "Vercel", + "url": "https://vercel.com", + "description": "Frontend cloud for building and deploying React, Vue, and static sites with global edge delivery.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Deploy React and Vue frontends to a global edge in minutes.", + "whyItMatters": "CAP treats React and Vue as first-class frontends. Vercel is a common host for those SPAs, giving developers preview deployments, edge functions, and CI-driven releases that pair well with a CAP backend on BTP.", + "personaTags": [ + "role:developer", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-1029-0001-000000000001", + "verb": "MODEL", + "shelf": "REFERENCE", + "sortOrder": 200, + "title": "Dremio", + "url": "https://www.dremio.com", + "description": "Lakehouse platform for SQL analytics directly on data lake storage, built around Apache Iceberg.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Query your data lake with SQL — no copies, no cubes.", + "whyItMatters": "Dremio is a leading open lakehouse engine. For teams federating SAP data with lake storage, it is a practical reference for Iceberg-based analytics alongside SAP Datasphere and Business Data Cloud.", + "personaTags": [ + "role:developer", + "role:architect", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-1029-0001-000000000002", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 210, + "title": "Dremio Community", + "url": "https://community.dremio.com", + "description": "Community forum for Dremio users — Q&A, how-tos, and release discussion.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Ask questions and share patterns with other Dremio users.", + "whyItMatters": "The Dremio community forum is where lakehouse practitioners troubleshoot Iceberg, reflections, and federation — useful when integrating lake data with SAP analytics.", + "personaTags": [ + "role:developer", + "role:architect" + ] + }, + { + "ID": "66333900-3rd0-1029-0001-000000000003", + "verb": "MODEL", + "shelf": "REFERENCE", + "sortOrder": 220, + "title": "Apache Iceberg", + "url": "https://iceberg.apache.org", + "description": "Open table format for huge analytic datasets — the storage standard behind modern lakehouses.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "The open table format underpinning modern lakehouses.", + "whyItMatters": "Iceberg is the table format SAP Business Data Cloud and many lake engines build on. Understanding it helps architects reason about how SAP and non-SAP data interoperate at the storage layer.", + "personaTags": [ + "role:developer", + "role:architect", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-1029-0001-000000000004", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 230, + "title": "Data Engineering Weekly", + "url": "https://www.dataengineeringweekly.com", + "description": "Curated weekly newsletter on data engineering trends, tools, and architecture.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Stay current on the wider data-engineering ecosystem.", + "whyItMatters": "A concise weekly read that keeps data architects aware of trends beyond the SAP stack — pipelines, formats, and platform shifts that influence integration choices.", + "personaTags": [ + "role:architect" + ] + }, + { + "ID": "66333900-3rd0-1029-0001-000000000005", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 240, + "title": "The Data Stack Show", + "url": "https://datastackshow.com", + "description": "Podcast with data engineers and founders on how modern data stacks are built and run.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Conversations on how real data stacks get built.", + "whyItMatters": "Practitioner interviews that surface the tradeoffs behind modern data platforms — helpful context for teams positioning SAP data products within a broader stack.", + "personaTags": [ + "role:developer", + "role:architect" + ] + }, + { + "ID": "66333900-3rd0-1029-0001-000000000006", + "verb": "MODEL", + "shelf": "KEEP_CURRENT", + "sortOrder": 250, + "title": "Reltio Community", + "url": "https://community.reltio.com", + "description": "Community for Reltio master-data-management practitioners — Q&A and best practices.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Master-data-management practices from the Reltio community.", + "whyItMatters": "MDM is a frequent companion to SAP data landscapes. The Reltio community is a reference point for entity resolution and data-quality patterns that complement SAP master data.", + "personaTags": [ + "role:developer", + "role:architect" + ] + }, + { + "ID": "66333900-3rd0-0005-0001-000000000001", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 200, + "title": "Hugging Face", + "url": "https://huggingface.co", + "description": "Hub for open models, datasets, and ML tooling — the de facto registry for the AI ecosystem.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "The open hub for models, datasets, and ML tooling.", + "whyItMatters": "Hugging Face is where most open models and datasets live. Developers building AI on BTP often source or evaluate models here before deploying via SAP AI Core.", + "personaTags": [ + "role:developer", + "role:student", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-0005-0001-000000000002", + "verb": "AI", + "shelf": "TOOLS", + "sortOrder": 210, + "title": "TabPFN (Prior Labs)", + "url": "https://github.com/PriorLabs/TabPFN", + "description": "Foundation model for tabular data that delivers strong results on small datasets without training.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "A foundation model for tabular data — no training required.", + "whyItMatters": "Most enterprise data is tabular. TabPFN is a notable open model for tabular prediction, relevant to developers exploring ML on structured SAP data.", + "personaTags": [ + "role:developer", + "role:student" + ] + }, + { + "ID": "66333900-3rd0-0005-0001-000000000003", + "verb": "AI", + "shelf": "REFERENCE", + "sortOrder": 220, + "title": "Prior Labs Research", + "url": "https://priorlabs.ai/research", + "description": "Research and technical reports behind TabPFN and tabular foundation models.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "The research behind tabular foundation models.", + "whyItMatters": "Prior Labs publishes the papers and reports underpinning TabPFN, including work featured in Nature — a primary source for developers evaluating the approach.", + "personaTags": [ + "role:developer", + "role:student" + ] + }, + { + "ID": "66333900-3rd0-0005-0001-000000000004", + "verb": "AI", + "shelf": "TOOLS", + "sortOrder": 230, + "title": "Kaggle", + "url": "https://www.kaggle.com", + "description": "Platform for datasets, notebooks, and ML competitions with a large practitioner community.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Datasets, notebooks, and competitions to sharpen ML skills.", + "whyItMatters": "Kaggle is a practical training ground for data science. Its datasets and notebooks help developers and students build the ML skills they later apply on SAP data.", + "personaTags": [ + "role:developer", + "role:student" + ] + }, + { + "ID": "66333900-3rd0-0003-0001-000000000001", + "verb": "INTEGRATE", + "shelf": "KEEP_CURRENT", + "sortOrder": 200, + "title": "n8n Community Forum", + "url": "https://community.n8n.io", + "description": "Official forum for the n8n workflow-automation community — Q&A, templates, and help.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Get help and share n8n automation workflows.", + "whyItMatters": "n8n is a popular open workflow-automation tool used to integrate SAP and non-SAP systems. Its forum is the primary place to find node patterns and troubleshoot flows.", + "personaTags": [ + "role:developer", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-0003-0001-000000000002", + "verb": "INTEGRATE", + "shelf": "KEEP_CURRENT", + "sortOrder": 210, + "title": "n8n Discord", + "url": "https://discord.gg/n8n", + "description": "Real-time chat community for n8n users and contributors.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Real-time chat with the n8n community.", + "whyItMatters": "The n8n Discord is where users get quick answers and share in-progress automations — a fast channel when building integrations that touch SAP endpoints.", + "personaTags": [ + "role:developer" + ] + }, + { + "ID": "66333900-3rd0-0003-0001-000000000003", + "verb": "INTEGRATE", + "shelf": "KEEP_CURRENT", + "sortOrder": 220, + "title": "n8n on YouTube", + "url": "https://www.youtube.com/c/n8n-io", + "description": "Official n8n channel — tutorials, feature demos, and automation walkthroughs.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Video tutorials and demos for n8n automation.", + "whyItMatters": "n8n's YouTube channel walks through building automations step by step — a fast way to learn node patterns before wiring up SAP integrations.", + "personaTags": [ + "role:developer" + ] + }, + { + "ID": "66333900-3rd0-0003-0001-000000000004", + "verb": "INTEGRATE", + "shelf": "TOOLS", + "sortOrder": 230, + "title": "n8n (n8n-io/n8n)", + "url": "https://github.com/n8n-io/n8n", + "description": "Source repository for the n8n workflow-automation platform.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "The open-source n8n automation engine on GitHub.", + "whyItMatters": "The n8n repo is the source of truth for the automation engine and its nodes — the place to file issues, read code, and understand how integrations execute.", + "personaTags": [ + "role:developer", + "deployment:cloud" + ] + }, + { + "ID": "66333900-3rd0-0003-0001-000000000005", + "verb": "INTEGRATE", + "shelf": "TOOLS", + "sortOrder": 240, + "title": "n8n docs (n8n-io/n8n-docs)", + "url": "https://github.com/n8n-io/n8n-docs", + "description": "Documentation source for n8n — node references and self-hosting guides.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Docs and node references for n8n, open to contributions.", + "whyItMatters": "The n8n-docs repo holds node references and self-hosting guides, and accepts public PRs — useful when documenting a custom SAP integration node.", + "personaTags": [ + "role:developer" + ] + }, + { + "ID": "66333900-3rd0-0006-0001-000000000001", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 200, + "title": "r/SAP", + "url": "https://www.reddit.com/r/SAP", + "description": "Reddit community discussing SAP products, careers, and day-to-day practice.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Candid SAP discussion from the wider practitioner community.", + "whyItMatters": "r/SAP is an unfiltered view of what SAP practitioners are dealing with — a useful pulse-check beyond official channels.", + "personaTags": [ + "role:developer" + ] + }, + { + "ID": "66333900-3rd0-0006-0001-000000000002", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 210, + "title": "r/dataengineering", + "url": "https://www.reddit.com/r/dataengineering", + "description": "Reddit community for data engineers — tooling debates, career, and architecture.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Where data engineers debate tools and architecture.", + "whyItMatters": "A high-signal community for data-platform trends and tradeoffs that inform how SAP data fits a broader engineering stack.", + "personaTags": [ + "role:architect" + ] + }, + { + "ID": "66333900-3rd0-0006-0001-000000000003", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 220, + "title": "r/MachineLearning", + "url": "https://www.reddit.com/r/MachineLearning", + "description": "Reddit community covering ML research, tooling, and applied practice.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "ML research and applied practice, community-curated.", + "whyItMatters": "Keeps developers aware of ML advances they may bring to SAP data — from model families to applied techniques.", + "personaTags": [ + "role:developer" + ] + }, + { + "ID": "66333900-3rd0-0006-0001-000000000004", + "verb": "CONNECT", + "shelf": "KEEP_CURRENT", + "sortOrder": 230, + "title": "r/n8n", + "url": "https://www.reddit.com/r/n8n", + "description": "Reddit community for n8n workflow automation — recipes and troubleshooting.", + "badge": null, + "isExternal": true, + "isActive": true, + "authoringStatus": "REVIEWED", + "personaWeight": 0, + "tagline": "Automation recipes and troubleshooting for n8n.", + "whyItMatters": "A community source for n8n automation recipes, complementing the official forum when building integrations across SAP and other systems.", + "personaTags": [ + "role:developer" + ] + } +]; diff --git a/srv/lib/homepage/seed-homepage-shelves.js b/srv/lib/homepage/seed-homepage-shelves.js new file mode 100644 index 000000000..8e5e7f7c7 --- /dev/null +++ b/srv/lib/homepage/seed-homepage-shelves.js @@ -0,0 +1,45 @@ +// srv/lib/homepage/seed-homepage-shelves.js +// +// Idempotent, NON-DESTRUCTIVE seed for the HomepageShelves table. +// +// Runs from cds.on('served') (srv/server.js) on every boot, in every profile +// (dev cds watch, unit tests via cds.test, hybrid, production). Inserts ONLY +// the baseline (verb,url) rows that are missing — it never updates or deletes +// existing rows, so admin edits made at /admin-ui/#homepage are preserved and a +// deploy can never full-replace the table. +// +// This replaces two fragile mechanisms: +// 1. the test/data seed CSV, which compiled to an .hdbtabledata with +// include_filter:[] and full-replaced the table on every deploy whose CSV +// hash changed (the pre-#1404c4c4 data-loss bug), and +// 2. the manual, DEV/PROD-ambiguous `npm run seed:thirdparty` promotion step. +// +// Single source of truth: ./homepage-shelves-defaults.js. + +import cds from '@sap/cds'; +import { HOMEPAGE_SHELVES_DEFAULTS } from './homepage-shelves-defaults.js'; + +const NAMESPACE = 'com.sap.developers.ims'; + +export async function seedHomepageShelves(dbOverride) { + const db = dbOverride ?? (await cds.connect.to('db')); + // Reflect the entity via cds.linked so CQL is type-aware and serializes the + // personaTags/personaHidden arrays to JSON for the HANA NCLOB columns. + // (cds.entities is undefined in a standalone context; a fully-qualified + // string name is not type-aware and fails on HANA for the array columns.) + const linked = cds.linked(cds.model ?? (await cds.load('*'))); + const { HomepageShelves } = linked.entities(NAMESPACE); + + const existing = await db.run( + SELECT.from(HomepageShelves).columns('verb', 'url'), + ); + const have = new Set(existing.map((r) => `${r.verb}|${r.url}`)); + const missing = HOMEPAGE_SHELVES_DEFAULTS.filter( + (r) => !have.has(`${r.verb}|${r.url}`), + ); + + if (missing.length > 0) { + await db.run(INSERT.into(HomepageShelves).entries(missing)); + } + return { inserted: missing.length, total: HOMEPAGE_SHELVES_DEFAULTS.length }; +} diff --git a/srv/server.js b/srv/server.js index 46b8a2338..0367a3558 100644 --- a/srv/server.js +++ b/srv/server.js @@ -1401,6 +1401,24 @@ cds.on('served', async () => { } } + // Seed the HomepageShelves baseline (verb-page shelf entries + footer links, + // including third-party content). Idempotent + non-destructive: inserts only + // missing (verb,url) rows, never overwrites admin edits. Replaces the retired + // seed CSV (whose .hdbtabledata full-replaced the table on deploy and wiped + // curated/third-party rows) and the manual `seed:thirdparty` step. Non-fatal. + if (!globalThis.__homepageShelvesSeeded) { + globalThis.__homepageShelvesSeeded = true; + try { + const { seedHomepageShelves } = await import('./lib/homepage/seed-homepage-shelves.js'); + const result = await seedHomepageShelves(cds.db); + if (result.inserted > 0) { + console.log(`[homepage-shelves] seeded ${result.inserted}/${result.total} baseline entries`); + } + } catch (err) { + console.warn('[homepage-shelves] seed failed (non-fatal):', err.message); + } + } + app.get('/auth/user', contextMw, authMw, async (req, res) => { // #1268: coarse deploy environment (DEV/PROD/QA/LOCAL) for the admin // header. Derived from the CF space name — safe to expose to anonymous diff --git a/test/data/com.sap.developers.ims-HomepageShelves.csv b/test/data/com.sap.developers.ims-HomepageShelves.csv deleted file mode 100644 index 8a61e08a8..000000000 --- a/test/data/com.sap.developers.ims-HomepageShelves.csv +++ /dev/null @@ -1,75 +0,0 @@ -ID;verb;shelf;sortOrder;title;url;description;badge;isExternal;isActive -66333900-0001-0001-0001-000000000001;LEARN;START_HERE;10;Tutorial Navigator;/tutorial-navigator/;Browse and filter 1 400+ hands-on tutorials across all SAP topics;;false;true -66333900-0001-0001-0001-000000000002;LEARN;START_HERE;20;SAP Learning Journeys;https://learning.sap.com/learning-journeys;Curated multi-course paths toward certifications and role mastery;;true;true -66333900-0001-0001-0001-000000000003;LEARN;START_HERE;30;New to Cloud SAP?;/missions/;Guided mission paths for developers moving from on-prem to cloud and AI;;false;true -66333900-0001-0001-0001-000000000004;LEARN;REFERENCE;40;learning.sap.com;https://learning.sap.com;Free and paid learning journeys, self-paced courses, and certification prep;;true;true -66333900-0001-0001-0001-000000000005;LEARN;REFERENCE;50;SAP Help Portal;https://help.sap.com;Centralized product documentation for all SAP products;;true;true -66333900-0001-0001-0001-000000000006;LEARN;REFERENCE;60;Certifications;https://www.sap.com/training-certification.html;Official SAP certification paths and instructor-led training;;true;true -66333900-0001-0001-0001-000000000007;LEARN;TOOLS;70;BTP Free Tier Signup;https://cockpit.btp.cloud.sap;Start building on SAP BTP with a free tier account — no credit card required;;true;true -66333900-0001-0001-0001-000000000008;LEARN;TOOLS;80;SAP-samples on GitHub;https://github.com/SAP-samples;Hundreds of sample apps, tutorials, and workshop repos from SAP;;true;true -66333900-0001-0001-0001-000000000009;LEARN;KEEP_CURRENT;90;SAP Developer News;https://youtube.com/@sapdevs;Weekly Friday show covering the latest developer news, tutorials, and events;;true;true -66333900-0001-0002-0001-000000000001;BUILD;START_HERE;10;SAP CAP;https://cap.cloud.sap;Official docs for the Cloud Application Programming Model — Node.js + Java;;true;true -66333900-0001-0002-0001-000000000002;BUILD;START_HERE;20;ABAP Cloud + RAP;https://help.sap.com/docs/abap-cloud;Clean-core ABAP development with RAP, ADT, and the BTP ABAP environment;;true;true -66333900-0001-0002-0001-000000000003;BUILD;START_HERE;30;Fiori / UI5;https://ui5.sap.com;SAP UI5 Demo Kit — API reference, samples, and interactive demos;;true;true -66333900-0001-0002-0001-000000000004;BUILD;START_HERE;40;SAP Build;https://www.sap.com/products/technology-platform/build.html;Low-code app, automation, and website suite on BTP for faster delivery;;true;true -66333900-0001-0002-0001-000000000005;BUILD;START_HERE;50;SAP Build Code;https://www.sap.com/products/technology-platform/build-code.html;AI-assisted pro-code development tooling on BTP with Joule co-pilot;;true;true -66333900-0001-0002-0001-000000000006;BUILD;REFERENCE;60;SAP Cloud SDK;https://sap.github.io/cloud-sdk/;Java + JavaScript SDKs for consuming SAP APIs from any cloud app;;true;true -66333900-0001-0002-0001-000000000007;BUILD;REFERENCE;70;Fiori Design System;https://www.sap.com/design-system/;SAP Fiori design guidelines for web, mobile, and Build apps;;true;true -66333900-0001-0002-0001-000000000008;BUILD;REFERENCE;80;Business Application Studio;https://help.sap.com/docs/SAP_BUSINESS_APPLICATION_STUDIO;Official docs for the cloud IDE purpose-built for SAP development;;true;true -66333900-0001-0002-0001-000000000009;BUILD;TOOLS;90;SAP Development Tools;https://tools.hana.ondemand.com;Download portal for Eclipse plugins, CLIs, HANA client, and SDKs;;true;true -66333900-0001-0002-0001-000000000010;BUILD;TOOLS;100;UI5 Web Components for React;https://sap.github.io/ui5-webcomponents-react/;React wrappers for UI5 web components — enterprise-grade UI in React;;true;true -66333900-0001-0002-0001-000000000011;BUILD;KEEP_CURRENT;110;SAP Tech Bytes;https://youtube.com/@sapdevs;Short-form, code-focused SAP Developers YouTube videos;;true;true -66333900-0001-0002-0001-000000000012;BUILD;KEEP_CURRENT;120;CAP Community Blogs;https://community.sap.com/t5/all-sap-community-blogs/ct-p/all-blogs;Latest community articles on CAP, ABAP, Fiori, and BTP development;;true;true -66333900-0001-0002-0001-000000000013;BUILD;KEEP_CURRENT;130;CodeJams;/tutorial-navigator/;Hands-on in-person and virtual workshops delivered by SAP experts;;false;true -66333900-0001-0003-0001-000000000001;INTEGRATE;START_HERE;10;SAP Business Accelerator Hub;https://api.sap.com;Browse and test SAP APIs across S/4HANA, SuccessFactors, Ariba, and more;;true;true -66333900-0001-0003-0001-000000000002;INTEGRATE;START_HERE;20;SAP Integration Suite;https://help.sap.com/docs/integration-suite;Docs for Cloud Integration, API Management, and Event Mesh on BTP;;true;true -66333900-0001-0003-0001-000000000003;INTEGRATE;START_HERE;30;Your First Integration Flow;/tutorial-navigator/;Step-by-step tutorials for building your first integration on SAP BTP;;false;true -66333900-0001-0003-0001-000000000004;INTEGRATE;REFERENCE;40;SAP Event Mesh;https://help.sap.com/docs/event-mesh;Managed message broker for event-driven architectures on BTP;;true;true -66333900-0001-0003-0001-000000000005;INTEGRATE;REFERENCE;50;SAP Destination Service;https://help.sap.com/docs/destination-service;BTP service that brokers credentials to external endpoints securely;;true;true -66333900-0001-0003-0001-000000000006;INTEGRATE;REFERENCE;60;Open Resource Discovery;https://open-resource-discovery.org;Open protocol for publishing and discovering application + service metadata;;true;true -66333900-0001-0003-0001-000000000007;INTEGRATE;REFERENCE;70;SAP OData Vocabularies;https://sap.github.io/odata-vocabularies/;OData annotation term definitions across SAP business domains;;true;true -66333900-0001-0003-0001-000000000008;INTEGRATE;TOOLS;80;Project Piper;https://www.project-piper.io/;Pre-built CI/CD pipelines and shared library for SAP application delivery;;true;true -66333900-0001-0003-0001-000000000009;INTEGRATE;TOOLS;90;Integration Samples;https://github.com/SAP-samples;Integration flow samples, API patterns, and event-driven architecture demos;;true;true -66333900-0001-0004-0001-000000000001;OPERATE;START_HERE;10;SAP BTP Cockpit;https://cockpit.btp.cloud.sap;Browser console for managing BTP global accounts, subaccounts, and spaces;;true;true -66333900-0001-0004-0001-000000000002;OPERATE;START_HERE;20;BTP CLI;https://help.sap.com/docs/btp-cli;Command-line reference for the `btp` tool — automate BTP account management;;true;true -66333900-0001-0004-0001-000000000003;OPERATE;START_HERE;30;BTP Getting Started;https://help.sap.com/docs/btp/sap-business-technology-platform/getting-started;Onboarding entry point and checklist for SAP Business Technology Platform;;true;true -66333900-0001-0004-0001-000000000004;OPERATE;REFERENCE;40;SAP BTP Documentation;https://help.sap.com/docs/btp;Canonical reference docs for SAP Business Technology Platform;;true;true -66333900-0001-0004-0001-000000000005;OPERATE;REFERENCE;50;SAP Discovery Center;https://discovery-center.cloud.sap;BTP service catalog, guided missions, estimator, and pricing;;true;true -66333900-0001-0004-0001-000000000006;OPERATE;REFERENCE;60;Kyma;https://kyma-project.io;Open-source Kubernetes platform for cloud-native BTP extensions;;true;true -66333900-0001-0004-0001-000000000007;OPERATE;REFERENCE;70;SAP HANA Cloud;https://help.sap.com/docs/SAP_HANA_CLOUD;Canonical docs for HANA Cloud in-memory database with vector engine;;true;true -66333900-0001-0004-0001-000000000008;OPERATE;TOOLS;80;Business Application Studio;https://help.sap.com/docs/SAP_BUSINESS_APPLICATION_STUDIO;Cloud IDE purpose-built for CAP, Fiori, mobile, and full-stack BTP development;;true;true -66333900-0001-0004-0001-000000000009;OPERATE;TOOLS;90;SAP Development Tools;https://tools.hana.ondemand.com;Download Eclipse plugins, CLIs, HANA client, and SDKs;;true;true -66333900-0001-0005-0001-000000000001;AI;START_HERE;10;BTP AI Best Practices;https://btp-ai-bp.docs.sap/;Curated guides and architecture patterns for building AI apps on BTP;;true;true -66333900-0001-0005-0001-000000000002;AI;START_HERE;20;AI Skills Library;https://skills.cloud.sap/;Searchable catalog of certified AI skills built on SAP BTP;;true;true -66333900-0001-0005-0001-000000000009;AI;START_HERE;25;Joule Studio;https://help.sap.com/docs/joule-studio;Low-code authoring environment for custom Joule skills, prompts, and tool integrations — next-generation edition (Q3 2026);NEW;true;true -66333900-0001-0005-0001-000000000003;AI;START_HERE;30;Your First Joule Extension;/tutorial-navigator/;Tutorial series for building custom Joule extensions with the AI SDK;;false;true -66333900-0001-0005-0001-000000000004;AI;REFERENCE;40;SAP Joule;https://help.sap.com/docs/joule;Docs for SAP's generative-AI copilot embedded across SAP products;;true;true -66333900-0001-0005-0001-000000000010;AI;REFERENCE;45;Joule Studio (Classic);https://help.sap.com/docs/joule-studio#classic;Classic Joule Studio edition — GA since December 2025 — for teams already shipping skills today;;true;true -66333900-0001-0005-0001-000000000005;AI;REFERENCE;50;SAP AI Core;https://help.sap.com/docs/sap-ai-core;Runtime for training and serving ML + GenAI models on BTP;;true;true -66333900-0001-0005-0001-000000000006;AI;REFERENCE;60;SAP AI Launchpad;https://help.sap.com/docs/sap-ai-launchpad;Web UI to manage AI scenarios, models, and deployments on BTP;;true;true -66333900-0001-0005-0001-000000000007;AI;TOOLS;70;AI4U Use Case Repository;https://ai4u-website.cfapps.eu10-004.hana.ondemand.com;50+ real-world AI projects built on SAP BTP — searchable by scenario;;true;true -66333900-0001-0005-0001-000000000008;AI;TOOLS;80;RAG on HANA Cookbook;https://github.com/SAP-samples;Retrieval-augmented generation patterns using HANA Cloud vector engine;;true;true -66333900-0001-0006-0001-000000000001;CONNECT;START_HERE;10;SAP Community;https://community.sap.com;Networking, Q&A, blogs, and groups for all SAP developer roles;;true;true -66333900-0001-0006-0001-000000000002;CONNECT;START_HERE;20;SAP Developers YouTube;https://youtube.com/@sapdevs;Tutorial videos, Tech Bytes, Developer News, and live streams;;true;true -66333900-0001-0006-0001-000000000003;CONNECT;START_HERE;30;Devtoberfest;https://community.sap.com/t5/devtoberfest/gh-p/Devtoberfest;Annual open developer celebration with sessions, challenges, and prizes;;true;true -66333900-0001-0006-0001-000000000004;CONNECT;REFERENCE;40;SAP News Center;https://news.sap.com;Official corporate news, customer stories, and product announcements;;true;true -66333900-0001-0006-0001-000000000005;CONNECT;REFERENCE;50;Community Blogs;https://community.sap.com/t5/all-sap-community-blogs/ct-p/all-blogs;Aggregated blog stream from the SAP Community;;true;true -66333900-0001-0006-0001-000000000006;CONNECT;REFERENCE;60;Developer Advocates;/developer-advocates/;Meet the SAP Developer Advocate team and find their content;;false;true -66333900-0001-0006-0001-000000000007;CONNECT;TOOLS;70;SAP on GitHub;https://github.com/SAP;SAP's open-source projects and repositories on GitHub;;true;true -66333900-0001-0006-0001-000000000008;CONNECT;TOOLS;80;SAP-samples on GitHub;https://github.com/SAP-samples;Sample code, tutorial repos, and workshop materials from SAP;;true;true -66333900-0001-0006-0001-000000000009;CONNECT;TOOLS;90;SAP-docs on GitHub;https://github.com/SAP-docs;Markdown sources for BTP/UI5/ABAP docs — public PRs welcome;HIDDEN_GEM;true;true -66333900-0001-0006-0001-000000000010;CONNECT;KEEP_CURRENT;100;SAP TechEd;https://www.sap.com/about/events/sap-teched.html;Annual developer conference with sessions, workshops, and hands-on labs;;true;true -66333900-0001-0006-0001-000000000011;CONNECT;KEEP_CURRENT;110;ASUG;https://www.asug.com;North America's largest SAP user community — events and networking;THIRD_PARTY;true;true -66333900-0001-0006-0001-000000000012;CONNECT;KEEP_CURRENT;120;SAPinsider;https://sapinsider.org;Independent SAP professional community publication and events;THIRD_PARTY;true;true -66333900-1029-1001-0001-000000000001;MODEL;START_HERE;10;SAP HANA Cloud;https://help.sap.com/docs/hana-cloud;Managed in-memory database with columnar analytics, spatial, and graph capabilities;;true;true -66333900-1029-1001-0001-000000000002;MODEL;START_HERE;20;SAP Datasphere;https://help.sap.com/docs/SAP_DATASPHERE;Business data fabric — model, federate, and govern data across SAP and third-party sources;;true;true -66333900-1029-1001-0001-000000000003;MODEL;START_HERE;30;SAP Business Data Cloud;https://www.sap.com/products/data-cloud/business-data-cloud.html;Unified data platform that combines SAP + Databricks for AI-ready business data;NEW;true;true -66333900-1029-1002-0001-000000000001;MODEL;REFERENCE;40;HANA Cloud Modeling Guide;https://help.sap.com/docs/hana-cloud/sap-hana-cloud-modeling-guide/sap-hana-cloud-modeling-guide;Reference for calculation views, HDI containers, and semantic modeling in HANA Cloud;;true;true -66333900-1029-1002-0001-000000000002;MODEL;REFERENCE;50;Datasphere Modeling;https://help.sap.com/docs/SAP_DATASPHERE/9f804b8efa8043539289f42f372c4862/459f2ecb37fd4c0299edac35e1670ac6.html;Build semantic views, data flows, and analytic models in Datasphere spaces;;true;true -66333900-1029-1002-0001-000000000003;MODEL;REFERENCE;60;SAP Analytics Cloud;https://help.sap.com/docs/SAP_ANALYTICS_CLOUD;Cloud analytics — stories, dashboards, planning, and predictive on top of your data models;;true;true -66333900-1029-1003-0001-000000000001;MODEL;TOOLS;70;SAP HANA Database Explorer;https://help.sap.com/docs/hana-cloud-database/sap-hana-database-explorer/sap-hana-database-explorer;Web-based SQL console, catalog browser, and modeling tool for HANA Cloud;;true;true -66333900-1029-1003-0001-000000000002;MODEL;TOOLS;80;hana-cli;https://www.npmjs.com/package/hana-cli;Command-line toolkit for HANA — SQL, table inspection, data profiling, and imports;;true;true -66333900-1029-1003-0001-000000000003;MODEL;TOOLS;90;Data Model Samples;https://github.com/SAP-samples?q=hana&type=all;Canonical HANA, Datasphere, and analytics data-model examples on SAP-samples;;true;true -66333900-1029-1004-0001-000000000001;MODEL;KEEP_CURRENT;100;HANA Cloud What's New;https://help.sap.com/docs/hana-cloud/sap-hana-cloud-what-s-new/sap-hana-cloud-what-s-new;Release notes for HANA Cloud — new features and quarterly capability additions;;true;true -66333900-1029-1004-0001-000000000002;MODEL;KEEP_CURRENT;110;Datasphere Roadmap;https://roadmaps.sap.com/board?PRODUCT=73555000100800002141;Public roadmap for SAP Datasphere — planned innovations and delivery timelines;;true;true -66333900-1029-1004-0001-000000000003;MODEL;KEEP_CURRENT;120;Data & Analytics Community;https://community.sap.com/t5/technology/ct-p/technology-blogs;SAP Community blogs and Q&A for HANA, Datasphere, BDC, and SAC practitioners;;true;true diff --git a/test/unit/db/admin-managed-seed-csvs.test.js b/test/unit/db/admin-managed-seed-csvs.test.js index 30b6b5438..c82d7c88b 100644 --- a/test/unit/db/admin-managed-seed-csvs.test.js +++ b/test/unit/db/admin-managed-seed-csvs.test.js @@ -17,12 +17,17 @@ const DB_DATA = join(import.meta.dirname, '../../../db/data'); const TEST_DATA = join(import.meta.dirname, '../../../test/data'); const ADMIN_MANAGED = [ - 'com.sap.developers.ims-HomepageShelves.csv', 'com.sap.developers.ims-HomepageConfig.csv', 'com.sap.developers.ims-CommunityBlogSources.csv', 'com.sap.developers.ims-ImsConfig.csv', ]; +// HomepageShelves no longer has a seed CSV at all — it is seeded idempotently +// (insert-if-missing on verb+url) at boot by srv/lib/homepage/seed-homepage-shelves.js +// from the inline canonical defaults (srv/lib/homepage/homepage-shelves-defaults.js). +// The CSV was retired so nothing can ever full-replace the admin-managed table. +const RETIRED_SHELVES_CSV = 'com.sap.developers.ims-HomepageShelves.csv'; + describe('admin-managed seed CSVs stay out of the production seed folder', () => { for (const csv of ADMIN_MANAGED) { it(`${csv} is NOT in db/data (would re-seed on deploy and wipe admin edits)`, () => { @@ -33,6 +38,13 @@ describe('admin-managed seed CSVs stay out of the production seed folder', () => }); } + it(`${RETIRED_SHELVES_CSV} is retired — absent from BOTH db/data and test/data`, () => { + // Seeded at boot (insert-if-missing), never from a CSV → an HDI redeploy + // can never full-replace the admin-managed HomepageShelves table. + expect(existsSync(join(DB_DATA, RETIRED_SHELVES_CSV))).toBe(false); + expect(existsSync(join(TEST_DATA, RETIRED_SHELVES_CSV))).toBe(false); + }); + it('Categories seed CSV header omits the editable seedDescription column', () => { // seedDescription is authored/edited at runtime (Categories UPDATE after-hook // → category-seed-embeddings). Keeping it in the seed CSV would let a diff --git a/test/unit/homepage/homepage-shelves-defaults.test.js b/test/unit/homepage/homepage-shelves-defaults.test.js new file mode 100644 index 000000000..05a3a11d4 --- /dev/null +++ b/test/unit/homepage/homepage-shelves-defaults.test.js @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { HOMEPAGE_SHELVES_DEFAULTS } from '../../../srv/lib/homepage/homepage-shelves-defaults.js'; +import { validateTags } from '../../../srv/lib/homepage/persona-tag-validator.js'; + +// Guards the inline canonical baseline that replaced the retired seed CSV + +// third-party staging JSON. No DB required — pure data validation, mirroring +// the checks the old scripts/__tests__/seed-thirdparty-data test performed. +const VERBS = new Set(['LEARN', 'BUILD', 'INTEGRATE', 'MODEL', 'OPERATE', 'AI', 'CONNECT']); +const SHELVES = new Set(['START_HERE', 'REFERENCE', 'TOOLS', 'KEEP_CURRENT']); +const BADGES = new Set(['NEW', 'UPDATED', 'HIDDEN_GEM', 'THIRD_PARTY']); + +describe('HOMEPAGE_SHELVES_DEFAULTS (canonical baseline)', () => { + it('every row has the mandatory fields with correct primitive types', () => { + for (const r of HOMEPAGE_SHELVES_DEFAULTS) { + expect(typeof r.verb, `verb for ${r.title}`).toBe('string'); + expect(typeof r.url, `url for ${r.title}`).toBe('string'); + expect(typeof r.title, `title for ${r.url}`).toBe('string'); + expect(r.title.length).toBeGreaterThan(0); + expect(typeof r.sortOrder, `sortOrder for ${r.title}`).toBe('number'); + expect(typeof r.isActive, `isActive for ${r.title}`).toBe('boolean'); + expect(typeof r.isExternal, `isExternal for ${r.title}`).toBe('boolean'); + } + }); + + it('every verb / shelf / badge value is a valid enum member', () => { + for (const r of HOMEPAGE_SHELVES_DEFAULTS) { + expect(VERBS, `verb ${r.verb}`).toContain(r.verb); + expect(SHELVES, `shelf ${r.shelf}`).toContain(r.shelf); + if (r.badge != null) expect(BADGES, `badge ${r.badge}`).toContain(r.badge); + } + }); + + it('(verb,url) is unique across all rows (matches @assert.unique.verbUrl)', () => { + const seen = new Set(); + for (const r of HOMEPAGE_SHELVES_DEFAULTS) { + const key = `${r.verb}|${r.url}`; + expect(seen.has(key), `duplicate (verb,url): ${key}`).toBe(false); + seen.add(key); + } + }); + + it('every personaTag is in the known vocabulary', () => { + for (const r of HOMEPAGE_SHELVES_DEFAULTS) { + if (r.personaTags?.length) { + const res = validateTags(r.personaTags); + expect(res.ok, `bad personaTags on ${r.title}: ${JSON.stringify(res.invalid)}`).toBe(true); + } + } + }); + + it('covers all 7 verbs and includes the curated third-party links', () => { + const verbs = new Set(HOMEPAGE_SHELVES_DEFAULTS.map((r) => r.verb)); + for (const v of VERBS) expect(verbs, `missing verb ${v}`).toContain(v); + const titles = HOMEPAGE_SHELVES_DEFAULTS.map((r) => r.title).join(' | '); + for (const t of ['Dremio', 'Reltio', 'Prior Labs', 'n8n']) { + expect(titles, `missing third-party link ${t}`).toContain(t); + } + }); +}); diff --git a/test/unit/homepage/seed-homepage-shelves.test.js b/test/unit/homepage/seed-homepage-shelves.test.js new file mode 100644 index 000000000..2f80bf68c --- /dev/null +++ b/test/unit/homepage/seed-homepage-shelves.test.js @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import cds from '@sap/cds'; +import { seedHomepageShelves } from '../../../srv/lib/homepage/seed-homepage-shelves.js'; +import { HOMEPAGE_SHELVES_DEFAULTS } from '../../../srv/lib/homepage/homepage-shelves-defaults.js'; + +cds.test('serve', '--project', '.', '--in-memory'); + +describe('seedHomepageShelves (idempotent, non-destructive boot seed)', () => { + let db; + beforeAll(async () => { db = await cds.connect.to('db'); }); + + it('boot (cds.on served) seeded the full baseline incl. third-party links', async () => { + const { HomepageShelves } = cds.entities('com.sap.developers.ims'); + const rows = await db.run(SELECT.from(HomepageShelves)); + expect(rows.length).toBe(HOMEPAGE_SHELVES_DEFAULTS.length); + const titles = rows.map((r) => r.title).join(' | '); + for (const t of ['Dremio', 'Reltio', 'Prior Labs', 'n8n']) { + expect(titles).toContain(t); + } + }); + + it('re-running the seed inserts nothing (idempotent on verb+url)', async () => { + const res = await seedHomepageShelves(db); + expect(res.inserted).toBe(0); + expect(res.total).toBe(HOMEPAGE_SHELVES_DEFAULTS.length); + }); + + it('never overwrites an existing (admin-edited) row', async () => { + const { HomepageShelves } = cds.entities('com.sap.developers.ims'); + const target = HOMEPAGE_SHELVES_DEFAULTS[0]; + const edited = 'ADMIN EDITED TITLE — do not clobber'; + await db.run( + UPDATE(HomepageShelves).set({ title: edited }).where({ verb: target.verb, url: target.url }), + ); + const before = (await db.run(SELECT.from(HomepageShelves))).length; + + await seedHomepageShelves(db); + + const row = await db.run( + SELECT.one.from(HomepageShelves).where({ verb: target.verb, url: target.url }), + ); + expect(row.title).toBe(edited); // untouched + const after = (await db.run(SELECT.from(HomepageShelves))).length; + expect(after).toBe(before); // no duplicate inserted + }); + + it('re-inserts a baseline row that was deleted (self-heals)', async () => { + const { HomepageShelves } = cds.entities('com.sap.developers.ims'); + const victim = HOMEPAGE_SHELVES_DEFAULTS[1]; + await db.run(DELETE.from(HomepageShelves).where({ verb: victim.verb, url: victim.url })); + expect( + await db.run(SELECT.one.from(HomepageShelves).where({ verb: victim.verb, url: victim.url })), + ).toBeUndefined(); + + const res = await seedHomepageShelves(db); + expect(res.inserted).toBe(1); + expect( + await db.run(SELECT.one.from(HomepageShelves).where({ verb: victim.verb, url: victim.url })), + ).toBeTruthy(); + }); +});