feat(domain): centralize data definition

This commit is contained in:
2025-11-17 00:04:27 +02:00
parent e7585aa76c
commit f39635e04f
96 changed files with 3474 additions and 1167 deletions
+4
View File
@@ -1,6 +1,7 @@
{
"dependencies": {
"@basango/db": "workspace:*",
"@basango/domain": "workspace:*",
"@basango/encryption": "workspace:*",
"@basango/logger": "workspace:*",
"@hono/node-server": "^1.19.6",
@@ -19,6 +20,9 @@
"exports": {
"./trpc/routers/_app": "./src/trpc/routers/_app.ts"
},
"imports": {
"#api/*": "./src/*"
},
"name": "@basango/api",
"private": true,
"scripts": {
+3 -6
View File
@@ -1,10 +1,10 @@
import { createArticle } from "@basango/db/queries";
import { createArticleResponseSchema, createArticleSchema } from "@basango/domain/models";
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
import type { Context } from "#api/rest/init";
import { withCrawlerAuth } from "#api/rest/middlewares/crawler";
import { withDatabase } from "#api/rest/middlewares/db";
import type { Context } from "#api/rest/types";
import { createArticleResponseSchema, createArticleSchema } from "#api/schemas/articles";
import { validateResponse } from "#api/utils/response";
const app = new OpenAPIHono<Context>();
@@ -44,10 +44,7 @@ app.openapi(
const input = c.req.valid("json");
const result = await createArticle(db, input);
return c.json(
validateResponse(result, createArticleResponseSchema) as { id: string; sourceId: string },
201,
);
return c.json(validateResponse(result, createArticleResponseSchema), 201);
},
);
-73
View File
@@ -1,73 +0,0 @@
import { z } from "@hono/zod-openapi";
const metadataSchema = z.object({
description: z.string().optional().openapi({
description: "A brief description or summary of the article.",
example: "This article discusses the latest advancements in AI technology.",
}),
image: z.url().optional().openapi({
description: "The URL of the main image associated with the article.",
example: "https://example.com/image.jpg",
}),
title: z.string().optional().openapi({
description: "The title of the article for metadata purposes.",
example: "The Rise of AI",
}),
});
export const createArticleSchema = z
.object({
body: z.string().min(1).openapi({
description: "The main content of the article.",
example: "This is the body of the article...",
}),
categories: z
.array(z.string())
.openapi({
description: "The categories or tags associated with the article.",
example: ["Technology", "AI"],
})
.optional()
.default([]),
hash: z.string().min(1).openapi({
description: "The unique hash of the article link.",
example: "d41d8cd98f00b204e9800998ecf8427e",
}),
link: z.url().openapi({
description: "The URL of the article.",
example: "https://example.com/article",
}),
metadata: metadataSchema.optional(),
publishedAt: z
.string()
.refine((value) => !Number.isNaN(Date.parse(value)), {
message: "Invalid date format",
})
.transform((value) => new Date(value))
.openapi({
description: "The publication date of the article in ISO 8601 format.",
example: "2023-01-01T00:00:00Z",
}),
sourceId: z.string().openapi({
description: "The unique identifier of the source from which the article was crawled.",
example: "radiookapi.net",
}),
title: z.string().min(1).openapi({
description: "The title of the article.",
example: "The Rise of AI",
}),
})
.openapi("CreateArticle");
export const createArticleResponseSchema = z
.object({
id: z.uuid().openapi({
description: "The unique identifier of the article.",
example: "b3b7c8e2-1f2a-4c3d-9e4f-5a6b7c8d9e0f",
}),
sourceId: z.uuid().openapi({
description: "The unique identifier of the source associated with the article.",
example: "a1a2b3c4-d5e6-7f8g-9h0i-j1k2l3m4n5o6",
}),
})
.openapi("CreateArticleResponse");
-106
View File
@@ -1,106 +0,0 @@
import { z } from "zod";
const idSchema = z.uuid().openapi({
description: "The unique identifier of the source.",
example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g",
});
const biasSchema = z.enum(["neutral", "slightly", "partisan", "extreme"]).openapi({
description: "The bias level of the source.",
example: "neutral",
});
const reliabilitySchema = z
.enum(["trusted", "reliable", "average", "low_trust", "unreliable"])
.openapi({
description: "The reliability level of the source.",
example: "trusted",
});
const transparencySchema = z.enum(["high", "medium", "low"]).openapi({
description: "The transparency level of the source.",
example: "high",
});
const credibilitySchema = z
.object({
bias: biasSchema.default("neutral"),
reliability: reliabilitySchema.default("average"),
transparency: transparencySchema.default("medium"),
})
.openapi({
description: "Credibility information about the source.",
});
export const createSourceSchema = z.object({
credibility: credibilitySchema.optional(),
description: z.string().max(1024).optional().openapi({
description: "A brief description of the source.",
example: "Radio Okapi is a Congolese radio station that provides news and information.",
}),
displayName: z.string().min(1).max(255).optional().openapi({
description: "The display name of the source.",
example: "Radio Okapi",
}),
name: z.string().min(1).max(255).openapi({
description: "The name of the source.",
example: "radiookapi.com",
}),
url: z.url().openapi({
description: "The URL of the source.",
example: "https://techcrunch.com",
}),
});
export const getSourceSchema = z.object({
id: idSchema,
});
export const getSourcePublicationGraphSchema = z.object({
days: z
.number()
.optional()
.openapi({
default: 60,
description: "",
example: 60,
})
.openapi({
description: "The number of days to include in the publication graph.",
}),
id: idSchema,
range: z
.object({
from: z.date().openapi({
description: "The start date of the range.",
}),
to: z.date().openapi({
description: "The end date of the range.",
}),
})
.optional()
.openapi({
description: "The date range for the publication graph.",
}),
});
export const getSourceCategorySharesSchema = z.object({
id: idSchema,
limit: z.number().int().min(1).max(100).optional().openapi({
default: 10,
description: "The maximum number of categories to return.",
example: 10,
}),
});
export const updateSourceSchema = z.object({
credibility: credibilitySchema.optional(),
description: createSourceSchema.shape.description,
displayName: createSourceSchema.shape.displayName,
id: idSchema,
name: createSourceSchema.shape.name.optional(),
});
export const createSourceResponseSchema = z.object({
id: idSchema,
...createSourceSchema.shape,
});
+26 -2
View File
@@ -1,10 +1,34 @@
import { createArticle } from "@basango/db/queries";
import {
createArticle,
getArticles,
getArticlesPublicationGraph,
getArticlesSourceDistribution,
} from "@basango/db/queries";
import {
createArticleSchema,
getArticlesSchema,
getDistributionsSchema,
getPublicationsSchema,
} from "@basango/domain/models";
import { createArticleSchema } from "#api/schemas/articles";
import { createTRPCRouter, protectedProcedure } from "#api/trpc/init";
export const articlesRouter = createTRPCRouter({
create: protectedProcedure.input(createArticleSchema).mutation(async ({ ctx, input }) => {
return createArticle(ctx.db, input);
}),
getPublications: protectedProcedure.input(getPublicationsSchema).query(async ({ ctx, input }) => {
return getArticlesPublicationGraph(ctx.db, input);
}),
getSourceDistribution: protectedProcedure
.input(getDistributionsSchema)
.query(async ({ ctx, input }) => {
return getArticlesSourceDistribution(ctx.db, input);
}),
list: protectedProcedure.input(getArticlesSchema).query(async ({ ctx, input }) => {
return getArticles(ctx.db, input);
}),
});
+8 -8
View File
@@ -6,14 +6,14 @@ import {
getSources,
updateSource,
} from "@basango/db/queries";
import {
createSourceSchema,
getSourceCategorySharesSchema,
getSourcePublicationGraphSchema,
getCategorySharesSchema,
getPublicationsSchema,
getSourceSchema,
updateSourceSchema,
} from "#api/schemas/sources";
} from "@basango/domain/models";
import { createTRPCRouter, protectedProcedure } from "#api/trpc/init";
export const sourcesRouter = createTRPCRouter({
@@ -21,24 +21,24 @@ export const sourcesRouter = createTRPCRouter({
return createSource(ctx.db, input);
}),
get: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)),
getById: protectedProcedure.input(getSourceSchema).query(async ({ ctx, input }) => {
return getSourceById(ctx.db, input.id);
}),
getCategoryShares: protectedProcedure
.input(getSourceCategorySharesSchema)
.input(getCategorySharesSchema)
.query(async ({ ctx, input }) => {
return getSourceCategoryShares(ctx.db, input);
}),
getPublicationGraph: protectedProcedure
.input(getSourcePublicationGraphSchema)
.input(getPublicationsSchema)
.query(async ({ ctx, input }) => {
return getSourcePublicationGraph(ctx.db, input);
}),
list: protectedProcedure.query(async ({ ctx }) => getSources(ctx.db)),
update: protectedProcedure.input(updateSourceSchema).mutation(async ({ ctx, input }) => {
return updateSource(ctx.db, input);
}),
-4
View File
@@ -1,4 +0,0 @@
export function parseInputValue(value?: string | null) {
if (value === null) return null;
return value ? JSON.parse(value) : undefined;
}
+8 -9
View File
@@ -1,20 +1,19 @@
import { logger } from "@basango/logger";
import { z } from "zod";
export function validateResponse(data: unknown, schema: z.ZodSchema) {
type ValidationSuccess<T> = z.infer<T>;
export function validateResponse<T extends z.ZodTypeAny>(
data: unknown,
schema: T,
): ValidationSuccess<T> {
const result = schema.safeParse(data);
if (!result.success) {
const cause = z.treeifyError(result.error);
logger.error({ cause }, "Response validation failed");
logger.error(cause);
return {
data: null,
details: cause,
error: "Response validation failed",
success: false,
};
throw new Error("Response validation failed");
}
return result.data;
-10
View File
@@ -1,14 +1,4 @@
{
"compilerOptions": {
"baseUrl": ".",
"composite": true,
"incremental": true,
"paths": {
"@basango/db": ["../../packages/db/src/*"],
"#api/*": ["./src/*"],
"#db/*": ["../../packages/db/src/*"]
}
},
"extends": "@basango/tsconfig/base.json",
"include": ["src"]
}