Skip to content

Commit 374a773

Browse files
authored
Cleans up rss feed and service, sets config during initialize (#22)
* clean up rss plugin and service * working rss feed and service with config set http * handle edge cases * nitpick and update docs
1 parent 046cf1d commit 374a773

File tree

13 files changed

+542
-298
lines changed

13 files changed

+542
-298
lines changed

apps/example/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"dependencies": {
1919
"@module-federation/node": "^2.6.22",
2020
"@types/express": "^5.0.0",
21-
"express": "^4.21.2"
21+
"@types/lodash": "^4.17.16",
22+
"express": "^4.21.2",
23+
"lodash": "^4.17.21"
2224
}
2325
}

apps/example/src/frontend/frontend.js

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -403,52 +403,45 @@ async function transformContent() {
403403
throw new Error("No transform plugins configured");
404404
}
405405

406-
let currentContent = content;
407-
updateTransformStatus("Starting transformations...", "info");
408-
409-
for (const [index, transformConfig] of config.transform.entries()) {
410-
updateTransformStatus(
411-
`Transforming with ${transformConfig.plugin} (${index + 1}/${config.transform.length})...`,
412-
"info",
413-
);
414-
415-
// Try to parse current content as JSON if it's a string that looks like JSON
416-
let parsedContent = currentContent;
417-
if (typeof currentContent === "string") {
418-
try {
419-
parsedContent = JSON.parse(currentContent);
420-
} catch (e) {
421-
// Not JSON, use as-is
422-
parsedContent = { content: currentContent };
423-
}
406+
// Try to parse content as JSON if it's a string that looks like JSON
407+
let parsedContent = content;
408+
if (typeof content === "string") {
409+
try {
410+
parsedContent = JSON.parse(content);
411+
} catch (e) {
412+
// Not JSON, use as-is
413+
parsedContent = { content };
424414
}
415+
}
425416

426-
const response = await fetch("/api/transform", {
427-
method: "POST",
428-
headers: {
429-
"Content-Type": "application/json",
430-
},
431-
body: JSON.stringify({
432-
plugin: transformConfig.plugin,
433-
config: transformConfig.config,
434-
content: parsedContent,
435-
}),
436-
});
417+
updateTransformStatus("Applying transformations...", "info");
437418

438-
if (!response.ok) {
439-
const error = await response.json();
440-
throw new Error(error.message || "Transform failed");
441-
}
419+
// Send all transforms at once
420+
const response = await fetch("/api/transform", {
421+
method: "POST",
422+
headers: {
423+
"Content-Type": "application/json",
424+
},
425+
body: JSON.stringify({
426+
transform: config.transform,
427+
content: parsedContent,
428+
}),
429+
});
442430

443-
const result = await response.json();
444-
currentContent = result.output;
445-
// Format the output for display
446-
contentEditor.value =
447-
typeof currentContent === "object"
448-
? JSON.stringify(currentContent, null, 2)
449-
: currentContent;
431+
if (!response.ok) {
432+
const error = await response.json();
433+
throw new Error(error.message || "Transform failed");
450434
}
451435

436+
const result = await response.json();
437+
const transformedContent = result.output;
438+
439+
// Format the output for display
440+
contentEditor.value =
441+
typeof transformedContent === "object"
442+
? JSON.stringify(transformedContent, null, 2)
443+
: transformedContent;
444+
452445
updateTransformStatus(
453446
"All transformations completed successfully",
454447
"success",

apps/example/src/index.ts

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
setPluginRegistry,
1010
} from "./plugin-service/plugin-registry";
1111
import { hydrateConfigValues } from "./utils";
12+
import { merge } from "lodash";
1213

1314
async function main() {
1415
const app = express();
@@ -92,26 +93,96 @@ async function main() {
9293
}
9394
});
9495

96+
/**
97+
* Combines transform results, merging objects or returning the new result
98+
*/
99+
function combineResults(prevResult: unknown, newResult: unknown): unknown {
100+
// If both are objects (not arrays), merge them with new values taking precedence
101+
if (
102+
typeof prevResult === "object" &&
103+
prevResult !== null &&
104+
!Array.isArray(prevResult) &&
105+
typeof newResult === "object" &&
106+
newResult !== null &&
107+
!Array.isArray(newResult)
108+
) {
109+
return merge({}, prevResult, newResult);
110+
}
111+
112+
// Otherwise return the new result (string will just return)
113+
return newResult;
114+
}
115+
116+
/**
117+
* Apply a series of transformations to content
118+
*/
119+
async function applyTransforms(
120+
content: any,
121+
transforms: Array<{ plugin: string; config: any }> = [],
122+
stage: string = "global",
123+
) {
124+
let result = content;
125+
126+
for (let i = 0; i < transforms.length; i++) {
127+
const transform = transforms[i];
128+
try {
129+
// Hydrate config with environment variables
130+
const hydratedConfig = hydrateConfigValues(transform.config);
131+
132+
// Load and configure transform plugin
133+
const plugin = await pluginService.getPlugin<"transformer">(
134+
transform.plugin,
135+
{
136+
type: "transformer",
137+
config: hydratedConfig,
138+
},
139+
);
140+
141+
console.log(
142+
`Applying ${stage} transform #${i + 1} (${transform.plugin})`,
143+
);
144+
const transformResult = await plugin.transform({
145+
input: result,
146+
config: hydratedConfig,
147+
});
148+
149+
// Validate transform output
150+
if (transformResult === undefined || transformResult === null) {
151+
throw new Error(
152+
`Transform ${transform.plugin} returned null or undefined`,
153+
);
154+
}
155+
156+
// Combine results, either merging objects or using new result
157+
result = combineResults(result, transformResult);
158+
} catch (error) {
159+
console.error(`Transform error (${transform.plugin}):`, error);
160+
throw new Error(
161+
`Transform failed at ${stage} stage, plugin ${transform.plugin}, index ${i}: ${
162+
error instanceof Error ? error.message : "Unknown error"
163+
}`,
164+
);
165+
}
166+
}
167+
168+
return result;
169+
}
170+
95171
// Transform endpoint
96172
app.post("/api/transform", async (req, res) => {
97173
try {
98-
const { plugin: pluginName, config: pluginConfig, content } = req.body;
174+
const { transform, content } = req.body;
99175

100176
if (!content) {
101177
throw new Error("No content provided for transformation");
102178
}
103179

104-
// Hydrate config with environment variables
105-
const hydratedConfig = hydrateConfigValues(pluginConfig);
106-
107-
// Load and configure transform plugin
108-
const plugin = await pluginService.getPlugin<"transformer">(pluginName, {
109-
type: "transformer",
110-
config: hydratedConfig,
111-
});
180+
if (!Array.isArray(transform) || transform.length === 0) {
181+
throw new Error("No transforms specified");
182+
}
112183

113-
// Transform content
114-
const result = await plugin.transform({ input: content });
184+
// Apply all transforms in sequence
185+
const result = await applyTransforms(content, transform);
115186
res.json({ success: true, output: result });
116187
} catch (error) {
117188
const errorMessage =

bun.lockb

384 Bytes
Binary file not shown.

docs/docs/plugins/distributors/rss.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,42 @@ Full configuration options for the RSS plugin:
6060
"plugin": "@curatedotfun/rss",
6161
"config": {
6262
"serviceUrl": "https://your-rss-service-url.com", // URL of your deployed RSS service
63-
"apiSecret": "{API_SECRET}" // Automatically injected from environment
63+
"apiSecret": "{API_SECRET}", // Automatically injected from environment
64+
"feedConfig": { // Optional feed configuration
65+
"title": "My Custom RSS Feed",
66+
"description": "A feed of curated content",
67+
"siteUrl": "https://example.com",
68+
"language": "en",
69+
"copyright": "© 2025",
70+
"maxItems": 50,
71+
"image": "https://example.com/logo.png",
72+
"author": {
73+
"name": "Feed Author",
74+
"email": "author@example.com",
75+
"link": "https://author.example.com"
76+
}
77+
}
6478
}
6579
}
6680
```
6781

82+
### Feed Configuration
83+
84+
The `feedConfig` property allows you to customize your RSS feed's metadata during plugin initialization. This configuration is sent to the RSS service to be available when feed data is queried.
85+
86+
Key configuration options:
87+
88+
- **title**: The title of your RSS feed
89+
- **description**: A brief description of your feed's content
90+
- **siteUrl**: The URL of your website (used for feed links)
91+
- **language**: The language code for your feed (e.g., "en", "es", "fr")
92+
- **copyright**: Copyright notice for your feed content
93+
- **maxItems**: Maximum number of items to keep in the feed (older items are removed)
94+
- **image**: URL to an image representing your feed
95+
- **author**: Information about the feed author (name, email, link)
96+
97+
If you don't provide a `feedConfig`, the RSS service will use default values.
98+
6899
## 🔄 Data Transformation
69100

70101
The RSS plugin accepts structured input data that maps to RSS item fields. You can provide as much or as little data as you want, and the plugin will fill in defaults for missing fields.

packages/rss/README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,42 @@ Full configuration options for the RSS plugin:
7070
"plugin": "@curatedotfun/rss",
7171
"config": {
7272
"serviceUrl": "https://your-rss-service-url.com", // URL of your deployed RSS service
73-
"apiSecret": "{API_SECRET}" // Automatically injected from environment
73+
"apiSecret": "{API_SECRET}", // Automatically injected from environment
74+
"feedConfig": { // Optional feed configuration
75+
"title": "My Custom RSS Feed",
76+
"description": "A feed of curated content",
77+
"siteUrl": "https://example.com",
78+
"language": "en",
79+
"copyright": "© 2025",
80+
"maxItems": 50,
81+
"image": "https://example.com/logo.png",
82+
"author": {
83+
"name": "Feed Author",
84+
"email": "author@example.com",
85+
"link": "https://author.example.com"
86+
}
87+
}
7488
}
7589
}
7690
```
7791

92+
### Feed Configuration
93+
94+
The `feedConfig` property allows you to customize your RSS feed's metadata during plugin initialization. This configuration is sent to the RSS service to be available when feed data is queried.
95+
96+
Key configuration options:
97+
98+
- **title**: The title of your RSS feed
99+
- **description**: A brief description of your feed's content
100+
- **siteUrl**: The URL of your website (used for feed links)
101+
- **language**: The language code for your feed (e.g., "en", "es", "fr")
102+
- **copyright**: Copyright notice for your feed content
103+
- **maxItems**: Maximum number of items to keep in the feed (older items are removed)
104+
- **image**: URL to an image representing your feed
105+
- **author**: Information about the feed author (name, email, link)
106+
107+
If you don't provide a `feedConfig`, the RSS service will use default values.
108+
78109
## 🔄 Data Transformation
79110

80111
The RSS plugin accepts structured input data that maps to RSS item fields. You can provide as much or as little data as you want, and the plugin will fill in defaults for missing fields.

packages/rss/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@curatedotfun/rss",
3-
"version": "0.0.9",
3+
"version": "0.0.10",
44
"description": "RSS plugin for curatedotfun",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

packages/rss/service/src/config.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import "dotenv/config";
2-
import fs from "fs";
3-
import path from "path";
42
import { FeedConfig } from "./types.js";
53

64
// Environment variables validation
@@ -65,23 +63,32 @@ const DEFAULT_CONFIG: FeedConfig = {
6563
author: { name: "Feed Author", email: "author@example.com" },
6664
};
6765

68-
// Load feed configuration from JSON file
69-
export function loadConfig(): FeedConfig {
70-
const CONFIG_FILE_PATH = path.join(process.cwd(), "feed-config.json");
71-
72-
try {
73-
const configFile = fs.readFileSync(CONFIG_FILE_PATH, "utf8");
74-
const config = JSON.parse(configFile) as FeedConfig;
75-
console.log("Loaded feed configuration from feed-config.json");
76-
return config;
77-
} catch (error) {
78-
console.warn(
79-
"Could not load feed-config.json, using default configuration",
80-
);
81-
return DEFAULT_CONFIG;
82-
}
66+
let currentConfig: FeedConfig | null = null;
67+
68+
// Set feed configuration
69+
export function setFeedConfig(config: FeedConfig): void {
70+
config.id = DEFAULT_FEED_ID;
71+
72+
// Set default values for optional fields if not provided
73+
config.title = config.title || DEFAULT_CONFIG.title;
74+
config.description = config.description || DEFAULT_CONFIG.description;
75+
config.siteUrl = config.siteUrl || DEFAULT_CONFIG.siteUrl;
76+
77+
// Ensure maxItems is always a positive number
78+
config.maxItems =
79+
typeof config.maxItems === "number" && config.maxItems > 0
80+
? config.maxItems
81+
: DEFAULT_CONFIG.maxItems;
82+
83+
config.language = config.language || DEFAULT_CONFIG.language;
84+
85+
// Update the in-memory configuration
86+
currentConfig = config;
87+
console.log("Updated feed configuration");
8388
}
8489

90+
// Get the current feed configuration
8591
export const getFeedConfig = (): FeedConfig => {
86-
return loadConfig();
92+
const config = currentConfig || DEFAULT_CONFIG;
93+
return JSON.parse(JSON.stringify(config)) as FeedConfig;
8794
};

packages/rss/service/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
handleGetItems,
1212
handleAddItem,
1313
handleHealth,
14+
handleUpdateConfig,
15+
handleGetConfig,
1416
} from "./routes.js";
1517
import { authenticate } from "./middleware.js";
1618
import { initializeFeed } from "./storage.js";
@@ -56,6 +58,8 @@ app.get("/feed.json", handleJsonFeed);
5658
app.get("/raw.json", handleRawJson);
5759
app.get("/api/items", handleGetItems);
5860
app.post("/api/items", handleAddItem);
61+
app.get("/api/config", handleGetConfig);
62+
app.put("/api/config", handleUpdateConfig);
5963

6064
// Initialize feed
6165
await initializeFeed();

0 commit comments

Comments
 (0)