Files
Announcements/server/src/app.js
T

183 lines
4.7 KiB
JavaScript

const express = require("express");
const {
createDetailedHealthRouter,
createHealthRouter,
} = require("./health/routes");
const { createAssetRouter } = require("./assets/routes");
const { createMediaRouter } = require("./assets/media-routes");
const { createAssetService } = require("./assets/service");
const { createPlaybackService } = require("./playback");
const { createPlaybackRouter } = require("./playback/routes");
const {
createPlaylistRouter,
createPlaylistService,
} = require("./playlists");
const { createSettingsRouter } = require("./settings/routes");
const { createSettingsService } = require("./settings/service");
function safeNotifier(notifier, logger, failureMessage) {
const warn = (error) => {
try {
logger?.warn?.(failureMessage, error);
} catch {
// A best-effort notification or warning must not undo a database commit.
}
};
return (event) => {
try {
const result = notifier(event);
result?.catch?.(warn);
} catch (error) {
warn(error);
}
};
}
function createApp({
config,
database,
imageProcessor,
lifecycle,
logger = console,
migrations,
now = Date.now,
onPlaylistsChanged = () => {},
playbackService: suppliedPlaybackService,
storage,
}) {
const app = express();
const notifyPlaylistsChanged = safeNotifier(
onPlaylistsChanged,
logger,
"Playlist invalidation notification failed",
);
const playbackService =
suppliedPlaybackService ||
createPlaybackService({
database,
now,
onActivationChanged: notifyPlaylistsChanged,
});
if (!suppliedPlaybackService) {
playbackService.initialize();
}
const reconcilePlaybackDomain = (change) =>
playbackService.domainChanged(change);
const reconcileBeforeDomainChange = () =>
playbackService.reconcile("before_domain_change");
const notifyPlaylistMutation = (change) => {
reconcilePlaybackDomain(change);
const publicChange = { ...change };
delete publicChange.invalidBoundaryChanged;
notifyPlaylistsChanged(publicChange);
};
const settingsService = createSettingsService(database);
const assetService = createAssetService({
database,
imageProcessor,
storage,
});
const playlistService = createPlaylistService({
database,
now,
onBeforeChange: reconcileBeforeDomainChange,
onChanged: notifyPlaylistMutation,
});
app.locals.playbackService = playbackService;
app.disable("x-powered-by");
app.set("trust proxy", config.trustProxy);
app.use(express.json({ limit: "1mb" }));
app.get("/api", (request, response) => {
response.json({ name: "Announcements API", version: 2 });
});
app.use(
"/health",
createHealthRouter({ database, lifecycle, migrations, storage }),
);
const detailedHealthRouterOptions = {
config,
database,
lifecycle,
migrations,
storage,
};
app.use(
"/api/health",
createDetailedHealthRouter(detailedHealthRouterOptions),
);
app.use(
"/api/status",
createDetailedHealthRouter(detailedHealthRouterOptions),
);
app.use(
"/api/settings",
createSettingsRouter(settingsService, {
onBeforePlaybackDomainChange: reconcileBeforeDomainChange,
onPlaylistsChanged: notifyPlaylistsChanged,
onPlaybackDomainChanged: reconcilePlaybackDomain,
}),
);
app.use(
"/api/assets",
createAssetRouter({
assetService,
onBeforePlaybackDomainChange: reconcileBeforeDomainChange,
onPlaylistsChanged: notifyPlaylistsChanged,
onPlaybackDomainChanged: reconcilePlaybackDomain,
processingConfig: config.processing,
}),
);
app.use(
"/api/playlists",
createPlaylistRouter(playlistService, {
playbackService,
}),
);
app.use("/api/playback", createPlaybackRouter(playbackService));
app.use("/media", createMediaRouter(assetService));
app.use((request, response) => {
response.status(404).json({
error: {
code: "NOT_FOUND",
message: "Route not found",
},
});
});
app.use((error, request, response, next) => {
if (response.headersSent) {
next(error);
return;
}
if (error instanceof SyntaxError && error.status === 400 && "body" in error) {
response.status(400).json({
error: {
code: "INVALID_JSON",
message: "Request body must contain valid JSON",
},
});
return;
}
const statusCode = error.statusCode || 500;
response.status(statusCode).json({
error: {
code: error.code || "INTERNAL_ERROR",
message:
statusCode >= 500 ? "An internal server error occurred" : error.message,
...(error.details ? { details: error.details } : {}),
},
});
});
return app;
}
module.exports = {
createApp,
};