implemented the domain logic + processor endpoints

This commit is contained in:
user
2026-03-01 06:33:32 +02:00
parent c74523e4bc
commit 8aa8ddfa7d
10 changed files with 1629 additions and 32 deletions

View File

@@ -7,10 +7,14 @@
"start": "node dist/index.js"
},
"dependencies": {
"@hono/node-server": "^1.19.9",
"@pkg/db": "workspace:*",
"@pkg/logger": "workspace:*",
"@pkg/logic": "workspace:*",
"@hono/node-server": "^1.19.9",
"hono": "^4.11.1"
"@pkg/result": "workspace:*",
"@pkg/settings": "workspace:*",
"hono": "^4.11.1",
"valibot": "^1.2.0"
},
"devDependencies": {
"@types/node": "^25.3.2",

View File

@@ -1,15 +1,266 @@
import {
pingMobileDeviceSchema,
registerMobileDeviceSchema,
syncMobileMediaSchema,
syncMobileSMSSchema,
} from "@pkg/logic/domains/mobile/data";
import { getMobileController } from "@pkg/logic/domains/mobile/controller";
import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context";
import { errorStatusMap, type Err } from "@pkg/result";
import { logDomainEvent } from "@pkg/logger";
import type { Context } from "hono";
import * as v from "valibot";
import { Hono } from "hono";
const mobileController = getMobileController();
const DEVICE_ID_HEADER = "x-device-id";
const FLOW_ID_HEADER = "x-flow-id";
function buildFlowExecCtx(c: Context): FlowExecCtx {
return {
flowId: c.req.header(FLOW_ID_HEADER) || crypto.randomUUID(),
};
}
function respondError(c: Context, fctx: FlowExecCtx, err: Err) {
const status = errorStatusMap[err.code] || 500;
c.status(status as any);
return c.json({ data: null, error: { ...err, flowId: fctx.flowId } });
}
function requireExternalDeviceId(c: Context): string | null {
const externalDeviceId = c.req.header(DEVICE_ID_HEADER);
if (!externalDeviceId || externalDeviceId.trim().length === 0) {
return null;
}
return externalDeviceId.trim();
}
async function parseJson(c: Context) {
try {
return await c.req.json();
} catch {
return null;
}
}
export const mobileRouter = new Hono()
.post("/register", async (c) => {
const fctx = buildFlowExecCtx(c);
const startedAt = Date.now();
logDomainEvent({
event: "processor.mobile.register.started",
fctx,
});
const payload = await parseJson(c);
const parsed = v.safeParse(registerMobileDeviceSchema, payload);
if (!parsed.success) {
const error = {
flowId: fctx.flowId,
code: "VALIDATION_ERROR",
message: "Invalid register payload",
description: "Please validate the payload and retry",
detail: parsed.issues.map((issue) => issue.message).join(", "),
} as Err;
return respondError(c, fctx, error);
}
const result = await mobileController.registerDevice(
fctx,
parsed.output,
);
if (result.isErr()) {
logDomainEvent({
level: "error",
event: "processor.mobile.register.failed",
fctx,
durationMs: Date.now() - startedAt,
error: result.error,
});
return respondError(c, fctx, result.error);
}
logDomainEvent({
event: "processor.mobile.register.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { deviceId: result.value.id },
});
return c.json({ data: result.value, error: null });
})
.put("/ping", async (c) => {
return c.json({ data: "" });
const fctx = buildFlowExecCtx(c);
const startedAt = Date.now();
const externalDeviceId = requireExternalDeviceId(c);
if (!externalDeviceId) {
const error = {
flowId: fctx.flowId,
code: "AUTH_ERROR",
message: "Missing device id header",
description: `Request must include ${DEVICE_ID_HEADER}`,
detail: `${DEVICE_ID_HEADER} header is required`,
} as Err;
return respondError(c, fctx, error);
}
const payload = await parseJson(c);
const parsed = v.safeParse(pingMobileDeviceSchema, payload ?? {});
if (!parsed.success) {
const error = {
flowId: fctx.flowId,
code: "VALIDATION_ERROR",
message: "Invalid ping payload",
description: "Please validate the payload and retry",
detail: parsed.issues.map((issue) => issue.message).join(", "),
} as Err;
return respondError(c, fctx, error);
}
const result = await mobileController.pingByExternalDeviceId(
fctx,
externalDeviceId,
parsed.output,
);
if (result.isErr()) {
logDomainEvent({
level: "error",
event: "processor.mobile.ping.failed",
fctx,
durationMs: Date.now() - startedAt,
error: result.error,
meta: { externalDeviceId },
});
return respondError(c, fctx, result.error);
}
logDomainEvent({
event: "processor.mobile.ping.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { externalDeviceId },
});
return c.json({ data: { ok: result.value }, error: null });
})
.put("/sms/sync", async (c) => {
return c.json({ data: "" });
const fctx = buildFlowExecCtx(c);
const startedAt = Date.now();
const externalDeviceId = requireExternalDeviceId(c);
if (!externalDeviceId) {
const error = {
flowId: fctx.flowId,
code: "AUTH_ERROR",
message: "Missing device id header",
description: `Request must include ${DEVICE_ID_HEADER}`,
detail: `${DEVICE_ID_HEADER} header is required`,
} as Err;
return respondError(c, fctx, error);
}
const payload = await parseJson(c);
const parsed = v.safeParse(syncMobileSMSSchema, payload);
if (!parsed.success) {
const error = {
flowId: fctx.flowId,
code: "VALIDATION_ERROR",
message: "Invalid SMS sync payload",
description: "Please validate the payload and retry",
detail: parsed.issues.map((issue) => issue.message).join(", "),
} as Err;
return respondError(c, fctx, error);
}
const result = await mobileController.syncSMSByExternalDeviceId(
fctx,
externalDeviceId,
parsed.output.messages,
);
if (result.isErr()) {
logDomainEvent({
level: "error",
event: "processor.mobile.sms_sync.failed",
fctx,
durationMs: Date.now() - startedAt,
error: result.error,
meta: {
externalDeviceId,
received: parsed.output.messages.length,
},
});
return respondError(c, fctx, result.error);
}
logDomainEvent({
event: "processor.mobile.sms_sync.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: {
externalDeviceId,
...result.value,
},
});
return c.json({ data: result.value, error: null });
})
.put("/media/sync", async (c) => {
return c.json({ data: "" });
})
.post("/register", async (c) => {
return c.json({ data: "" });
const fctx = buildFlowExecCtx(c);
const startedAt = Date.now();
const externalDeviceId = requireExternalDeviceId(c);
if (!externalDeviceId) {
const error = {
flowId: fctx.flowId,
code: "AUTH_ERROR",
message: "Missing device id header",
description: `Request must include ${DEVICE_ID_HEADER}`,
detail: `${DEVICE_ID_HEADER} header is required`,
} as Err;
return respondError(c, fctx, error);
}
const payload = await parseJson(c);
const parsed = v.safeParse(syncMobileMediaSchema, payload);
if (!parsed.success) {
const error = {
flowId: fctx.flowId,
code: "VALIDATION_ERROR",
message: "Invalid media sync payload",
description: "Please validate the payload and retry",
detail: parsed.issues.map((issue) => issue.message).join(", "),
} as Err;
return respondError(c, fctx, error);
}
const result = await mobileController.syncMediaByExternalDeviceId(
fctx,
externalDeviceId,
parsed.output.assets,
);
if (result.isErr()) {
logDomainEvent({
level: "error",
event: "processor.mobile.media_sync.failed",
fctx,
durationMs: Date.now() - startedAt,
error: result.error,
meta: {
externalDeviceId,
received: parsed.output.assets.length,
},
});
return respondError(c, fctx, result.error);
}
logDomainEvent({
event: "processor.mobile.media_sync.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: {
externalDeviceId,
...result.value,
},
});
return c.json({ data: result.value, error: null });
});

View File

@@ -1,13 +1,30 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": true,
"verbatimModuleSyntax": false,
"skipLibCheck": true,
"types": [
"node"
],
"baseUrl": ".",
"paths": {
"@pkg/logic": ["../../packages/logic"],
"@pkg/logic/*": ["../../packages/logic/*"],
"@pkg/db": ["../../packages/db"],
"@pkg/db/*": ["../../packages/db/*"],
"@pkg/logger": ["../../packages/logger"],
"@pkg/logger/*": ["../../packages/logger/*"],
"@pkg/result": ["../../packages/result"],
"@pkg/result/*": ["../../packages/result/*"],
"@pkg/settings": ["../../packages/settings"],
"@pkg/settings/*": ["../../packages/settings/*"],
"@/*": ["../../packages/logic/*"],
"@core/*": ["../../packages/logic/core/*"],
"@domains/*": ["../../packages/logic/domains/*"]
},
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"outDir": "./dist"