I'm having difficulty with type definitions on my Bun server using ElysiaJS, specifically with the body property within the handler function. I've created two models, a User and a Character model, and I've created two controllers for each respectively. When creating the characters controller I had run into this issue with typescript, typescript error. An error stating that I couldn't spread an unknown variable type into an object. What's interesting is that I never defined a body type in the users controller, at least not with my patch update route, which I had copied over into my character controller. In the Users controller, I had grouped user creation and login under Elysia's Guard method, which defined strictly what the request body would look like. However, according to Elysia's docs, anything outside of Guards method would not be 'guarded', which I found to be the case when testing, as I could leave password out and only update username. With user creation the character is created automatically, and all the attributes within character are not required, but are defaulted to be updated later. I have read through the documentation and I have not been able to find out how to create body schemas that have optional attributes. Something like this:
body: t.Object({
username: t.String(),
password: t.String(),
visualMode?: t.String() //Optional (t is an Elysian method)
})
I am also wondering why it's working for my users controller to an extent. When I had tested it, I only tested username and password, but when I had tried to use optional attributes, like visualMode, it failed. There isn't anything in my Users controller that defines the body type aside from the Guard method, but for some reason it is reaching outside of it's scope. From what I've heard, Bun and Elysia are stable but have issues that arise from time to time. I'm not sure if this is an ElysiaJS issue or if it's me misinterpreting things, because I have not found anything in the documentation to help me with this issue.
Here is my User controller
//Imports
import { Elysia, t } from 'elysia';
import { jwt } from '@elysiajs/jwt';
//Interfaces and Models
import { IUser, UserModel } from '../models/users.model';
import { CharacterModel } from '../models/character.model';
export const usersController = new Elysia().group('/users', (app) =>
app
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET as string,
}),
)
.guard(
{
body: t.Object({
username: t.String(),
password: t.String(),
}),
},
(app) =>
app
.post('/', async ({ body, jwt, set }) => {
const isTaken = await UserModel.findOne({
username: body.username,
});
if (!isTaken) {
try {
const character = await CharacterModel.create(
{},
);
const user = await UserModel.create({
username: body.username,
password: body.password,
character: character._id,
});
const accessToken = await jwt.sign({
userId: user._id,
});
set.headers = {
'X-Authorization': accessToken,
};
set.status = 201;
return user;
} catch (e: any) {
let retArr = [];
if ('errors' in e) {
for (const key in e.errors) {
retArr.push(e.errors[key].message);
}
return retArr;
} else {
set.status = 500;
return e;
}
}
} else {
set.status = 203;
return {
message: 'That username is already in use.',
};
}
})
.post('/login', async ({ body, jwt, set }) => {
const user = await UserModel.findOne({
username: body.username,
});
if (!user) {
set.status = 204;
return {
message: 'Username does not exist.',
};
} else {
const verify = await Bun.password.verify(
body.password,
user.password,
);
if (verify) {
const accessToken = await jwt.sign({
userId: user._id,
});
set.headers = {
'X-Authorization': accessToken,
};
set.status = 201;
return {
message: 'User has logged in.',
};
} else {
return {
message: 'Password is incorrect.',
};
}
}
}),
)
.patch(
'/:id',
async ({ body, jwt, request, set, params: { id } }) => {
const accessToken = request.headers.get('X-Authorization');
if (!accessToken) {
set.status = 204;
return {
message: 'You are not authorized.',
};
} else {
if ((await jwt.verify(accessToken)) == false) {
return {
message: 'You are not authorized.',
};
}
try {
const changes: Partial<IUser> = { ...body };
const updatedUser = await UserModel.findByIdAndUpdate(
id,
changes,
{
new: true,
runValidators: true,
},
);
set.status = 201;
return updatedUser;
} catch (e: any) {
let retArr = [];
if ('errors' in e) {
for (const key in e.errors) {
retArr.push(e.errors[key].message);
}
return retArr;
} else {
set.status = 500;
return e;
}
}
}
},
{
beforeHandle: ({ set, params: { id } }) => {
if (id.length !== 24) {
set.status = 405;
return {
message: 'User ID is not valid.',
};
}
},
},
)
.delete(
'/:id',
async ({ jwt, request, set, params: { id } }) => {
const accessToken = request.headers.get('X-Authorization');
if (!accessToken) {
set.status = 204;
return {
message: 'You are not authorized.',
};
} else {
if ((await jwt.verify(accessToken)) == false) {
return {
message: 'You are not authorized.',
};
}
try {
const user = await UserModel.findById(id);
await CharacterModel.findByIdAndDelete(user!.character);
await UserModel.findByIdAndDelete(id);
set.status = 201;
return {
message: 'User has been deleted.',
};
} catch (e: any) {
let retArr = [];
if ('errors' in e) {
for (const key in e) {
retArr.push(e.errors[key].message);
}
return retArr;
} else {
set.status = 500;
return e;
}
}
}
},
{
beforeHandle: ({ set, params: { id } }) => {
if (id.length !== 24) {
set.status = 405;
return {
message: 'User ID is not valid.',
};
}
},
},
)
.get('/', async ({ set }) => {
const users = await UserModel.find({});
if (!users) {
set.status = 204;
return {
message: 'There are no users.',
};
} else {
set.status = 201;
return users;
}
})
.get(
'/:id',
async ({ params: { id }, set }) => {
const user = await UserModel.findById(id);
if (!user) {
set.status = 204;
return {
message: 'User ID does not exist.',
};
} else {
set.status = 201;
return user;
}
},
{
beforeHandle: ({ set, params: { id } }) => {
if (id.length !== 24) {
set.status = 405;
return {
message: 'User ID is not valid.',
};
}
},
},
),
);
Character Controller:
//Imports
import { Elysia, t } from 'elysia';
//Models
import { ICharacter, CharacterModel } from '../models/character.model';
import { IUser, UserModel } from '../models/users.model';
import { jwt } from '@elysiajs/jwt';
export const charactersController = new Elysia().group('/characters', (app) =>
app
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET as string,
}),
)
.patch(
'/:id',
async ({ body, jwt, request, set, params: { id } }) => {
const accessToken = request.headers.get('X-Authorization');
if (!accessToken) {
set.status = 204;
return {
message: 'You are not authorized.',
};
} else {
if ((await jwt.verify(accessToken)) == false) {
return {
message: 'You are not authorized.',
};
}
try {
const changes: Partial<ICharacter> = { ...body };
const updatedCharacter =
await CharacterModel.findByIdAndUpdate(
id,
changes,
{
new: true,
runValidators: true,
},
);
set.status = 201;
return updatedCharacter;
} catch (e: any) {
let retArr = [];
if ('errors' in e) {
for (const key in e.errors) {
retArr.push(e.errors[key].message);
}
return retArr;
} else {
set.status = 500;
return e;
}
}
}
},
{
beforeHandle: ({ set, params: { id } }) => {
if (id.length !== 24) {
set.status = 405;
return {
message: 'User ID is not valid.',
};
}
},
},
)
.get('/', async ({ set }) => {
const characters = await CharacterModel.find({});
if (!characters) {
set.status = 204;
return {
message: 'There are no characters.',
};
} else {
set.status = 201;
return characters;
}
})
.get(
'/:id',
async ({ set, params: { id } }) => {
const character = await CharacterModel.findById(id);
if (!character) {
set.status = 204;
return {
message: 'Character ID does not exist.',
};
} else {
set.status = 201;
return character;
}
},
{
beforeHandle: ({ set, params: { id } }) => {
if (id.length !== 24) {
set.status = 405;
return {
message: 'User ID is not valid.',
};
}
},
},
),
);
Character Model:
//Imports
import { Schema, Types, model, Model, Document} from 'mongoose';
//Character Interfaces
interface ICharacterAttribute {
level: number,
xp: number,
xpToNextLevel: number
}
export interface ICharacter extends Document{
charIcon?: string,
strength?: ICharacterAttribute,
dexterity?: ICharacterAttribute,
constitution?: ICharacterAttribute,
wisdom?: ICharacterAttribute,
intelligence?: ICharacterAttribute,
charisma?: ICharacterAttribute,
xpPerMinute?: number,
consecutiveMultiplier?: number
}
//Typescript casting
type CharacterModelType = Model<ICharacter>;
//Character Schema
const characterSchema = new Schema<ICharacter> ({
charIcon : String,
strength : {
level: {
type: Number,
default: 1
},
xp: {
type: Number,
default: 0
},
xpToNextLevel: {
type: Number,
default: 100
}
},
dexterity : {
level: {
type: Number,
default: 1
},
xp: {
type: Number,
default: 0
},
xpToNextLevel: {
type: Number,
default: 100
}
},
constitution : {
level: {
type: Number,
default: 1
},
xp: {
type: Number,
default: 0
},
xpToNextLevel: {
type: Number,
default: 100
}
},
wisdom : {
level: {
type: Number,
default: 1
},
xp: {
type: Number,
default: 0
},
xpToNextLevel: {
type: Number,
default: 100
}
},
intelligence : {
level: {
type: Number,
default: 1
},
xp: {
type: Number,
default: 0
},
xpToNextLevel: {
type: Number,
default: 100
}
},
charisma : {
level: {
type: Number,
default: 1
},
xp: {
type: Number,
default: 0
},
xpToNextLevel: {
type: Number,
default: 100
}
},
xpPerMinute : { type: Number, default: 5 },
consecutiveMultiplier : { type: Number, default: 1 },
})
export const CharacterModel = model<ICharacter, CharacterModelType>('Character', characterSchema);
User Model:
//Imports
import { Schema, Types, model, Model, Document} from 'mongoose';
//User Interface
export interface IUser extends Document{
username: string,
password: string,
visualMode?: string,
character?: Types.ObjectId,
xpPerMinute?: number,
consecutiveMultiplier?: number,
}
type UserModelType = Model<IUser>
//User Schema
const userSchema = new Schema<IUser>({
username : {
type: String,
required: [true, 'Username is required.'],
minlength: [5, 'Username must be over 4 characters.'],
maxlength: [15, 'Username is too long.']
},
password : {
type: String,
required: [true, 'Password is required.'],
minlength: [5, 'Password must be over 4 characters.']
},
visualMode : String,
character : Types.ObjectId,
//"tasks" : [/*list of task id's*/],
}, {
timestamps: true
})
//Middleware
userSchema.pre('save', function(next) {
Bun.password.hash(this.password)
.then(hash => {
this.password = hash;
next()
})
})
export const UserModel = model<IUser, UserModelType>('User', userSchema)
I've tried following Elysia's documentation around Schemas, but I was not able to figure out anything conclusive. I really want to be able to define optional attributes within Elysia's body handler, because I might update one attribute but not the others. If you have any suggestions/refactoring please let me know.
I was able to find an answer, whether it was intended or not, the guard method hard parses routes within it's scope, and provides type definitions to the rest of the routes within the exported function. I used that to parse the body for my characters controller, and the t import was re-exported from another library with better documentation (@sinclair/typebox), I found that you can add optional tags like this: