2
0

feed-parser.ts 1.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. import { z } from 'zod';
  2. import loggerFactory from '~/utils/logger';
  3. const logger = loggerFactory('growi:feature:news:feed-parser');
  4. const FeedItemSchema = z.object({
  5. id: z.string().min(1),
  6. type: z.string().optional(),
  7. emoji: z.string().optional(),
  8. title: z.record(z.string()),
  9. body: z.record(z.string()).optional(),
  10. url: z.string().optional(),
  11. publishedAt: z.string().min(1),
  12. conditions: z
  13. .object({
  14. targetRoles: z.array(z.string()).optional(),
  15. growiVersionRegExps: z.array(z.string()).optional(),
  16. })
  17. .optional(),
  18. });
  19. const FeedJsonSchema = z.object({
  20. version: z.string(),
  21. // Items are parsed individually so a single bad item does not abort the batch
  22. items: z.array(z.unknown()),
  23. });
  24. export type FeedItem = z.infer<typeof FeedItemSchema>;
  25. export interface FeedJson {
  26. version: string;
  27. items: FeedItem[];
  28. }
  29. /**
  30. * Validate parsed JSON against the feed schema.
  31. * Items failing per-item validation are skipped (logged), allowing the rest to be processed.
  32. * Returns null when the top-level shape is invalid.
  33. */
  34. export const parseFeedJson = (raw: unknown): FeedJson | null => {
  35. const topResult = FeedJsonSchema.safeParse(raw);
  36. if (!topResult.success) {
  37. logger.error(
  38. { issues: topResult.error.issues },
  39. 'News feed JSON top-level shape invalid',
  40. );
  41. return null;
  42. }
  43. const validItems: FeedItem[] = [];
  44. for (const rawItem of topResult.data.items) {
  45. const itemResult = FeedItemSchema.safeParse(rawItem);
  46. if (itemResult.success) {
  47. validItems.push(itemResult.data);
  48. } else {
  49. logger.warn(
  50. { issues: itemResult.error.issues },
  51. 'News feed item failed validation, skipping',
  52. );
  53. }
  54. }
  55. return { version: topResult.data.version, items: validItems };
  56. };