Saltar al contenido principal

Version 3.0 release notes

· 7 min de lectura
Loïc Poullain
Creator of FoalTS. Software engineer.

Banner

Version 3.0 of Foal is finally there!

It's been a long work and I'm excited to share with you the new features of the framework 🎉 . The upgrading guide can be found here.

Here are the new features and improvements of version 3!

Full support of TypeORM v0.3

For those new to Foal, TypeORM is the default ORM used in all new projects. But you can use any other ORM or query builder if you want, as the core framework is ORM independent.

TypeORM v0.3 provides greater typing safety and this is something that will be appreciated when moving to the new version of Foal.

The version 0.3 of TypeORM has a lot of changes compared to the version 0.2 though. Features such as the ormconfig.json file have been removed and functions such as createConnection, getManager or getRepository have been deprecated.

A lot of work has been done to make sure that @foal/typeorm, new projects generated by the CLI and examples in the documentation use version 0.3 of TypeORM without relying on deprecated functions or patterns.

In particular, the connection to the database is now managed by a file src/db.ts that replaces the older ormconfig.json.

Code simplified

Some parts of the framework have been simplified to require less code and make it more understandable.

Authentication

The @UseSessions and @JWTRequired authentication hooks called obscure functions such as fetchUser, fetchUserWithPermissions to populate the ctx.user property. The real role of these functions was not clear and a newcomer to the framework could wonder what they were for.

This is why these functions have been removed and replaced by direct calls to database models.

// Version 2
@UseSessions({ user: fetchUser(User) })
@JWTRequired({ user: fetchUserWithPermissions(User) })

// Version 3
@UseSessions({ user: (id: number) => User.findOneBy({ id }) })
@JWTRequired({ user: (id: number) => User.findOneWithPermissionsBy({ id }) })

File upload

When uploading files in a multipart/form-data request, it was not allowed to pass optional fields. This is now possible.

The interface of the @ValidateMultipartFormDataBody hook, renamed to @ParseAndValidateFiles to be more understandable for people who don't know the HTTP protocol handling the upload, has been simplified.

Examples with only files

// Version 2
@ValidateMultipartFormDataBody({
files: {
profile: { required: true }
}
})

// Version 3
@ParseAndValidateFiles({
profile: { required: true }
})

Examples with files and fields

// Version 2
@ValidateMultipartFormDataBody({
files: {
profile: { required: true }
}
fields: {
description: { type: 'string' }
}
})

// Version 3
@ParseAndValidateFiles(
{
profile: { required: true }
},
// The second parameter is optional
// and is used to add fields. It expects an AJV object.
{
type: 'object',
properties: {
description: { type: 'string' }
},
required: ['description'],
additionalProperties: false
}
)

Database models

Using functions like getRepository or getManager to manipulate data in a database is not necessarily obvious to newcomers. It adds complexity that is not necessary for small or medium sized projects. Most frameworks prefer to use the Active Record pattern for simplicity.

This is why, from version 3 and to take into account that TypeORM v0.3 no longer uses a global connection, the examples in the documentation and the generators will extend all the models from BaseEntity. Of course, it will still be possible to use the functions below if desired.

// Version 2
@Entity()
class User {}

const user = getRepository(User).create();
await getRepository(User).save(user);

// Version 3
@Entity()
class User extends BaseEntity {}

const user = new User();
await user.save();

Better typing

The use of TypeScript types has been improved and some parts of the framework ensure better type safety.

Validation with AJV

Foal's version uses ajv@8 which allows you to bind a TypeScript type with a JSON schema object. To do this, you can import the generic type JSONSchemaType to build the interface of the schema object.

import { JSONSchemaType } from 'ajv';

interface MyData {
foo: number;
bar?: string
}

const schema: JSONSchemaType<MyData> = {
type: 'object',
properties: {
foo: { type: 'integer' },
bar: { type: 'string', nullable: true }
},
required: ['foo'],
additionalProperties: false
}

File upload

In version 2, handling file uploads in the controller was tedious because all types were any. Starting with version 3, it is no longer necessary to cast the types to File or File[]:

// Version 2
const name = ctx.request.body.fields.name;
const file = ctx.request.body.files.avatar as File;
const files = ctx.request.body.files.images as File[];

// After
const name = ctx.request.body.name;
// file is of type "File"
const file = ctx.files.get('avatar')[0];
// files is of type "Files"
const files = ctx.files.get('images');

Authentication

In version 2, the user option of @UseSessions and @JWTRequired expected a function with this signature:

(id: string|number, services: ServiceManager) => Promise<any>;

There was no way to guess and guarantee the type of the user ID and the function had to check and convert the type itself if necessary.

The returned type was also very permissive (type any) preventing us from detecting silly errors such as confusion between null and undefined values.

In version 3, the hooks have been added a new userIdType option to check and convert the JavaScript type if necessary and force the TypeScript type of the function. The returned type is also safer and corresponds to the type of ctx.user which is no longer any but { [key : string] : any } | null.

Example where the ID is a string

@JWTRequired({
user: (id: string) => User.findOneBy({ id });
userIdType: 'string',
})

Example where the ID is a number

@JWTRequired({
user: (id: number) => User.findOneBy({ id });
userIdType: 'number',
})

By default, the value of userIdType is a number, so we can simply write this:

@JWTRequired({
user: (id: number) => User.findOneBy({ id });
})

GraphQL

In version 2, GraphQL schemas were of type any. In version 3, they are all based on the GraphQLSchema interface.

Closer to JS ecosystem standards

Some parts have been modified to get closer to the JS ecosystem standards. In particular:

Development command

The npm run develop has been renamed to npm run dev.

Configuration through environment variables

When two values of the same variable are provided by a .env file and an environment variable, then the value of the environment is used (the behavior is similar to that of the dotenv library).

null vs undefined values

When the request has no session or the user is not authenticated, the values of ctx.session and ctx.user are null and no longer undefined. This makes sense from a semantic point of view, and it also simplifies the user assignment from the find functions of popular ORMs (Prisma, TypeORM, Mikro-ORM). They all return null when no value is found.

More open to other ORMs

TypeORM is the default ORM used in the documentation examples and in the projects generated by the CLI. But it is quite possible to use another ORM or query generator with Foal. For example, the authentication system (with sessions or JWT) makes no assumptions about database access.

Some parts of the framework were still a bit tied to TypeORM in version 2. Version 3 fixed this.

Shell scripts

When running the foal generate script command, the generated script file no longer contains TypeORM code.

Permission system

The @PermissionRequired option is no longer bound to TypeORM and can be used with any ctx.user that implements the IUserWithPermissions interface.

Smaller AWS S3 package

The @foal/aws-s3 package is now based on version 3 of the AWS SDK. Thanks to this, the size of the node_modules has been reduced by three.

Dependencies updated and support of Node latest versions

All Foal's dependencies have been upgraded. The framework is also tested on Node versions 16 and 18.

Some bug fixes

If the configuration file production.js explicitly returns undefined for a given key and the default.json file returns a defined value for this key, then the value from the default.json file is returned by Config.get.