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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion db/channels.cds
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ entity Channels : cuid, managed {
isSapOwned : Boolean default false;
category : String(60);
subcategory : String(80);
platform : String(40);
platform : String(80);
status : ChannelStatus default 'Active' @assert.range;
focusAreas : array of String(60);
tags : array of String(40);
Expand Down
150 changes: 75 additions & 75 deletions db/last-dev/csn.json
Original file line number Diff line number Diff line change
Expand Up @@ -2145,7 +2145,7 @@
},
"platform": {
"type": "cds.String",
"length": 40,
"length": 80,
"@cds.persistence.name": "PLATFORM"
},
"status": {
Expand Down Expand Up @@ -2431,6 +2431,80 @@
},
"@cds.persistence.name": "COM_SAP_DEVELOPERS_IMS_CHANNELTOPICMAP"
},
"com.sap.developers.ims.ChannelSubmissions": {
"kind": "entity",
"@cds.persistence.journal": true,
"elements": {
"ID": {
"key": true,
"type": "cds.String",
"length": 36,
"@cds.persistence.name": "ID"
},
"createdAt": {
"type": "cds.Timestamp",
"@cds.persistence.name": "CREATEDAT"
},
"createdBy": {
"type": "cds.String",
"length": 255,
"@cds.persistence.name": "CREATEDBY"
},
"modifiedAt": {
"type": "cds.Timestamp",
"@cds.persistence.name": "MODIFIEDAT"
},
"modifiedBy": {
"type": "cds.String",
"length": 255,
"@cds.persistence.name": "MODIFIEDBY"
},
"kind": {
"type": "cds.String",
"length": 5000,
"@cds.persistence.name": "KIND"
},
"targetChannel_ID": {
"type": "cds.String",
"length": 36,
"@odata.foreignKey4": "targetChannel",
"@cds.persistence.name": "TARGETCHANNEL_ID"
},
"proposed": {
"type": "cds.LargeString",
"@cds.persistence.name": "PROPOSED"
},
"rationale": {
"type": "cds.String",
"length": 1000,
"@cds.persistence.name": "RATIONALE"
},
"submitterId": {
"type": "cds.String",
"length": 120,
"@cds.persistence.name": "SUBMITTERID"
},
"status": {
"type": "cds.String",
"default": {
"val": "PENDING"
},
"length": 5000,
"@cds.persistence.name": "STATUS"
},
"reviewerId": {
"type": "cds.String",
"length": 120,
"@cds.persistence.name": "REVIEWERID"
},
"reviewNote": {
"type": "cds.String",
"length": 800,
"@cds.persistence.name": "REVIEWNOTE"
}
},
"@cds.persistence.name": "COM_SAP_DEVELOPERS_IMS_CHANNELSUBMISSIONS"
},
"com.sap.developers.ims.FailedEmails": {
"kind": "entity",
"@cds.persistence.journal": true,
Expand Down Expand Up @@ -3733,80 +3807,6 @@
},
"@cds.persistence.name": "COM_SAP_DEVELOPERS_IMS_UIEVENT"
},
"com.sap.developers.ims.ChannelSubmissions": {
"kind": "entity",
"@cds.persistence.journal": true,
"elements": {
"ID": {
"key": true,
"type": "cds.String",
"length": 36,
"@cds.persistence.name": "ID"
},
"createdAt": {
"type": "cds.Timestamp",
"@cds.persistence.name": "CREATEDAT"
},
"createdBy": {
"type": "cds.String",
"length": 255,
"@cds.persistence.name": "CREATEDBY"
},
"modifiedAt": {
"type": "cds.Timestamp",
"@cds.persistence.name": "MODIFIEDAT"
},
"modifiedBy": {
"type": "cds.String",
"length": 255,
"@cds.persistence.name": "MODIFIEDBY"
},
"kind": {
"type": "cds.String",
"length": 5000,
"@cds.persistence.name": "KIND"
},
"targetChannel_ID": {
"type": "cds.String",
"length": 36,
"@odata.foreignKey4": "targetChannel",
"@cds.persistence.name": "TARGETCHANNEL_ID"
},
"proposed": {
"type": "cds.LargeString",
"@cds.persistence.name": "PROPOSED"
},
"rationale": {
"type": "cds.String",
"length": 1000,
"@cds.persistence.name": "RATIONALE"
},
"submitterId": {
"type": "cds.String",
"length": 120,
"@cds.persistence.name": "SUBMITTERID"
},
"status": {
"type": "cds.String",
"default": {
"val": "PENDING"
},
"length": 5000,
"@cds.persistence.name": "STATUS"
},
"reviewerId": {
"type": "cds.String",
"length": 120,
"@cds.persistence.name": "REVIEWERID"
},
"reviewNote": {
"type": "cds.String",
"length": 800,
"@cds.persistence.name": "REVIEWNOTE"
}
},
"@cds.persistence.name": "COM_SAP_DEVELOPERS_IMS_CHANNELSUBMISSIONS"
},
"com.sap.developers.ims.CatGameAwards": {
"kind": "entity",
"doc": "\"Hit the Cat\" mini-game daily point ledger (issue #2042).\n\nOne row = one calendar day on which a signed-in player earned the daily\ncat-game award during a given Devtoberfest event. The award endpoint\n(POST /api/devtoberfest/cat-game/award) writes exactly one row per\n(user, event, day): 5 points per day, once per day, capped at 100 points\ntotal per event, and only while that event is active.\n\nThe natural (user, event, awardDate) tuple is the PRIMARY KEY — no surrogate\nID — so HANA enforces the once-per-day rule at the DB layer: a second insert\nfor the same day collides on the PK and the handler maps it to\n\"already-today\" (race-safe, unlike a CAP-only @assert.unique which this\nrepo's raw db.run() inserts bypass — see srv/lib/resolve-db-user.js).\n\n`createdAt` (from `managed`) is the actual award instant and is what the\ncross-container GAMEBOARD_BONUS_V1 view exposes as AWARD_DATE, so the\ngameboard's event-window filter always sees an in-window timestamp (the\nendpoint only ever inserts while the event is live).",
Expand Down
8 changes: 6 additions & 2 deletions db/src/com.sap.developers.ims.Channels.hdbmigrationtable
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
== version=1
== version=2
COLUMN TABLE com_sap_developers_ims_Channels (
ID NVARCHAR(36) NOT NULL,
createdAt TIMESTAMP,
Expand All @@ -17,7 +17,7 @@ COLUMN TABLE com_sap_developers_ims_Channels (
isSapOwned BOOLEAN DEFAULT FALSE,
category NVARCHAR(60),
subcategory NVARCHAR(80),
platform NVARCHAR(40),
platform NVARCHAR(80),
status NVARCHAR(5000) DEFAULT 'Active',
focusAreas NCLOB,
tags NCLOB,
Expand All @@ -34,3 +34,7 @@ COLUMN TABLE com_sap_developers_ims_Channels (
lastChecked TIMESTAMP,
PRIMARY KEY(ID)
)

== migration=2
-- generated by cds-compiler version 7.0.1
ALTER TABLE com_sap_developers_ims_Channels ALTER (platform NVARCHAR(80));
2 changes: 1 addition & 1 deletion scripts/seed-channels.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// npx cds bind --exec -- node scripts/seed-channels.cjs --file d:/tmp/External-SAP-Channels-Complete.json --commit
const cds = require('@sap/cds');
const { readFileSync } = require('node:fs');
const { normalizeChannel } = require('../srv/lib/channels/normalize.js');
const { normalizeChannel } = require('../srv/lib/channels/normalize.cjs');

const CURATED = ['isPublished', 'isFeatured', 'editorialNote', 'linkStatus', 'linkStatusOverride', 'lastChecked'];

Expand Down
20 changes: 17 additions & 3 deletions srv/lib/channels/normalize.js → srv/lib/channels/normalize.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ function normalizeStatus(raw) {
return { status: 'Active', note: s };
}

// Coerce an approximate count ("~1.4K", "3,200", "1.7K", 806) to an integer.
// The research dataset gives GitHub-star counts as human-readable approximations;
// the Channels.githubStars/subscribers columns are Integer. Returns null when
// there is nothing parseable.
function parseApproxCount(raw) {
if (raw == null) return null;
if (typeof raw === 'number') return Number.isFinite(raw) ? Math.round(raw) : null;
const s = String(raw).trim().replace(/^~/, '').replace(/,/g, '');
const m = s.match(/^([0-9]*\.?[0-9]+)\s*([kKmMbB]?)$/);
if (!m) return null;
const mult = { '': 1, k: 1e3, m: 1e6, b: 1e9 }[m[2].toLowerCase()];
return Math.round(parseFloat(m[1]) * mult);
}

// Hash only the source (dataset-owned) fields, order-independent.
function computeContentHash(sourceFields) {
const canonical = JSON.stringify(sourceFields, Object.keys(sourceFields).sort());
Expand All @@ -64,10 +78,10 @@ function normalizeChannel(raw, ingestBatch) {
focusAreas: raw.focus_areas ?? [],
tags: raw.tags ?? [],
updateFrequency: raw.update_frequency ?? null,
githubStars: raw.github_stars ?? null,
subscribers: raw.subscribers ?? null,
githubStars: parseApproxCount(raw.github_stars),
subscribers: parseApproxCount(raw.subscribers),
};
return { sourceId: raw.id, ...source, contentHash: computeContentHash(source), ingestBatch };
}

module.exports = { cleanCitations, normalizeOwnerType, normalizeStatus, computeContentHash, normalizeChannel };
module.exports = { cleanCitations, normalizeOwnerType, normalizeStatus, parseApproxCount, computeContentHash, normalizeChannel };
23 changes: 21 additions & 2 deletions test/channels-normalize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { describe, it, expect } from 'vitest';
import {
cleanCitations, normalizeOwnerType, normalizeStatus,
computeContentHash, normalizeChannel,
} from '../srv/lib/channels/normalize.js';
parseApproxCount, computeContentHash, normalizeChannel,
} from '../srv/lib/channels/normalize.cjs';

describe('channels normalize', () => {
it('strips [cite:] markers and trailing space', () => {
Expand All @@ -24,6 +24,25 @@ describe('channels normalize', () => {
.toEqual({ status: 'Active', note: 'Canonical source' });
});

it('parses approximate counts to integers for the Integer columns', () => {
expect(parseApproxCount('~1.4K')).toBe(1400);
expect(parseApproxCount('~3.2K')).toBe(3200);
expect(parseApproxCount('~520')).toBe(520);
expect(parseApproxCount('1,234')).toBe(1234);
expect(parseApproxCount(806)).toBe(806);
expect(parseApproxCount(null)).toBeNull();
expect(parseApproxCount('n/a')).toBeNull();
});

it('normalizeChannel coerces github_stars/subscribers to integers', () => {
const row = normalizeChannel({
id: 'gh-1', name: 'Repo', url: 'https://x',
github_stars: '~1.4K', subscribers: 806,
}, '2026-09-03');
expect(row.githubStars).toBe(1400);
expect(row.subscribers).toBe(806);
});

it('content hash is stable across key order and changes with content', () => {
const a = computeContentHash({ name: 'X', url: 'u', purpose: 'p' });
const b = computeContentHash({ url: 'u', purpose: 'p', name: 'X' });
Expand Down
2 changes: 1 addition & 1 deletion test/channels-seed.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// test/channels-seed.test.js
import cds from '@sap/cds';
import { describe, it, expect, afterAll } from 'vitest';
import { normalizeChannel } from '../srv/lib/channels/normalize.js';
import { normalizeChannel } from '../srv/lib/channels/normalize.cjs';

const project = cds.test('serve', '--project', '.', '--in-memory');
const NS = 'com.sap.developers.ims';
Expand Down
Loading