Skip to main content

Version 3.0 release notes

ยท 7 min read
Loรฏc Poullain

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.