implemented the domain logic + processor endpoints
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user