mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
`vite build` failed on main and had for a while: useSkillFavorites imports
useGetSkillFavoritesQuery and useUpdateSkillFavoritesMutation from ~/data-provider
and neither name has ever existed, so rollup stopped at that import and the client
bundle was never produced. Nor did the /favorites/skills route and the second
MAX_SKILL_FAVORITES cap they were written against exist on the server. The whole
feature was addressed to an API nobody wrote.
A favourited skill is a favourite. It goes in the ONE list the user already has —
the same array, the same /v1/chat/user/settings/favorites route, the same 50 cap —
as `{ skillId }`, a sibling of `{ agentId }` and `{ model, endpoint }`. The read
projects the skill slice out, the write merges it back, so favourites of other
kinds ride through a skill toggle untouched. useGetFavoritesQuery gains a TData
parameter to carry the projection; nothing else changes for its existing callers.
The second kind is `spec`, and that one was a live rejection rather than a build
break. useFavorites has had addFavoriteSpec/toggleFavoriteSpec since before this,
and the controller answered every one of them 400 "Each favorite must have either
agentId or model+endpoint" — a favourited model spec could not be saved. The
schema had no `spec` field either, so mongoose would have dropped it even past the
validator. FavoritesController.spec.js already described the correct validator in
full, down to the refusal messages; the controller was simply an older, narrower
one. Implementing what the spec says takes it from 17 failed / 11 passed to 28 / 28.
Three of those asserted messages now name skillId as well, because the kind list is
written once (KINDS) and every refusal reads it — the spec was written before this
kind existed and could not have listed it.
Client build: green, `client/dist/index.html` present. Gates barrel, credentials
and chrome pass. tsc --noEmit 762 -> 758.
126 lines
4.2 KiB
JavaScript
126 lines
4.2 KiB
JavaScript
const { updateUser, getUserById } = require('~/models');
|
|
|
|
const MAX_FAVORITES = 50;
|
|
const MAX_STRING_LENGTH = 256;
|
|
/** The kinds a favorite may be, written once so no two refusals disagree. */
|
|
const KINDS = 'agentId, skillId, model+endpoint, or spec';
|
|
|
|
const updateFavoritesController = async (req, res) => {
|
|
try {
|
|
const { favorites } = req.body;
|
|
const userId = req.user.id;
|
|
|
|
if (!favorites) {
|
|
return res.status(400).json({ message: 'Favorites data is required' });
|
|
}
|
|
|
|
if (!Array.isArray(favorites)) {
|
|
return res.status(400).json({ message: 'Favorites must be an array' });
|
|
}
|
|
|
|
if (favorites.length > MAX_FAVORITES) {
|
|
return res.status(400).json({
|
|
code: 'MAX_FAVORITES_EXCEEDED',
|
|
message: `Maximum ${MAX_FAVORITES} favorites allowed`,
|
|
limit: MAX_FAVORITES,
|
|
});
|
|
}
|
|
|
|
for (const fav of favorites) {
|
|
for (const field of ['agentId', 'skillId', 'model', 'endpoint']) {
|
|
if (fav[field] && fav[field].length > MAX_STRING_LENGTH) {
|
|
return res
|
|
.status(400)
|
|
.json({ message: `${field} exceeds maximum length of ${MAX_STRING_LENGTH}` });
|
|
}
|
|
}
|
|
/* `spec` is the one field with a shape rule of its own: null and undefined
|
|
* mean absent, anything else must be a non-empty string. */
|
|
if (fav.spec != null && (typeof fav.spec !== 'string' || fav.spec.length === 0)) {
|
|
return res.status(400).json({ message: 'spec must be a non-empty string' });
|
|
}
|
|
if (typeof fav.spec === 'string' && fav.spec.length > MAX_STRING_LENGTH) {
|
|
return res
|
|
.status(400)
|
|
.json({ message: `spec exceeds maximum length of ${MAX_STRING_LENGTH}` });
|
|
}
|
|
|
|
/* One entry names exactly ONE thing. `model` and `endpoint` are halves of
|
|
* a single name, so they count once and must both be present. */
|
|
const hasAgent = !!fav.agentId;
|
|
const hasSkill = !!fav.skillId;
|
|
const hasSpec = typeof fav.spec === 'string' && fav.spec.length > 0;
|
|
const hasModelPart = !!fav.model || !!fav.endpoint;
|
|
const hasModel = !!fav.model && !!fav.endpoint;
|
|
const kinds = [hasAgent, hasSkill, hasSpec, hasModel].filter(Boolean).length;
|
|
|
|
if (kinds > 1) {
|
|
return res.status(400).json({ message: `Favorite cannot have multiple types (${KINDS})` });
|
|
}
|
|
if (hasSpec && (hasAgent || hasSkill || hasModelPart)) {
|
|
return res.status(400).json({
|
|
message: 'spec cannot be combined with agentId, skillId, model, or endpoint',
|
|
});
|
|
}
|
|
if (hasAgent && hasModelPart) {
|
|
return res.status(400).json({
|
|
message: 'agentId cannot be combined with model or endpoint',
|
|
});
|
|
}
|
|
if (hasSkill && hasModelPart) {
|
|
return res.status(400).json({
|
|
message: 'skillId cannot be combined with model or endpoint',
|
|
});
|
|
}
|
|
if (hasModelPart && !hasModel) {
|
|
return res.status(400).json({ message: 'model and endpoint must be provided together' });
|
|
}
|
|
if (kinds === 0) {
|
|
return res.status(400).json({ message: `Each favorite must have one of: ${KINDS}` });
|
|
}
|
|
}
|
|
|
|
const user = await updateUser(userId, { favorites });
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ message: 'User not found' });
|
|
}
|
|
|
|
res.status(200).json(user.favorites);
|
|
} catch (error) {
|
|
console.error('Error updating favorites:', error);
|
|
res.status(500).json({ message: 'Internal server error' });
|
|
}
|
|
};
|
|
|
|
const getFavoritesController = async (req, res) => {
|
|
try {
|
|
if (req.user?.guest === true) {
|
|
return res.status(200).json([]);
|
|
}
|
|
const userId = req.user.id;
|
|
const user = await getUserById(userId, 'favorites');
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ message: 'User not found' });
|
|
}
|
|
|
|
let favorites = user.favorites || [];
|
|
|
|
if (!Array.isArray(favorites)) {
|
|
favorites = [];
|
|
await updateUser(userId, { favorites: [] });
|
|
}
|
|
|
|
res.status(200).json(favorites);
|
|
} catch (error) {
|
|
console.error('Error fetching favorites:', error);
|
|
res.status(500).json({ message: 'Internal server error' });
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
updateFavoritesController,
|
|
getFavoritesController,
|
|
};
|