diceline-chartmagnifierquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

How to type-safely interact with Firestore documents

The problem

When using Typescript and Firestore, we usually have to do a lot of manual casting when working with documents. One such example would be getting the data of a document:

const thread = threadDocument.data(); // this will be of type any

Should we want to interact with the data in a type-safe manner, we'll have to cast it, which can quickly become tedious.

const thread = <ThreadData>threadDocument.data();

Additionally, when we write data to Firestore, there are no restrictions on how the data should look.

The solution

This is when Firestore Data Converters can come in handy. All we have to do is implement two methods - one where we constrain the data that gets written and one where we cast the data coming from Firestore:

const converter = {
  toFirestore: (dataToBeWritten: ThreadData) => data,
  fromFirestore: (document: QueryDocumentSnapshot) => <ThreadData>document.data(),
};

To take this one step further, we can store the "converted" collection reference so we won't have to apply the converters each time we query the collection:

const threadCollection = db.collection("threads").withConverter(converter);

Now we can safely interact with the collection without having to cast the data:

const threadDocument = await threadCollection.doc(id).get();
const thread = threadDocument.data(); // this will be of type ThreadData

How to install npm packages from your local machine

If you need to manually test a package you are developing and don't want to make a release each time you modify something, you can install it directly from your local machine.

You can achieve this by using the following commands, depending on the package manager you use:

yarn add [path-to-your-package]
npm install [path-to-your-package]

This will have the following equivalent in package.json:

...
"dependencies": {
    "[package]": "file:[relative-path-to-package]",
    ...
},

How to use Laravel's Tappable trait to achieve fluency

Given the following schemas:

Schema::create('skills', function (Blueprint $table) {
    $table->id();
    $table->string("name");
});
//pivot table
Schema::create('category_skill', function (Blueprint $table) {
    $table->id();
    $table->foreignId("skill_id")->constrained();
    $table->foreignId("category_id")->constrained();
});
//the categories table will be omitted for brevity

Suppose you have to write a method which created a skill, attached multiple categories to it and returned the created skill afterwards:

class CreateSkillAction
{
    public function execute(SkillDTO $skillData): Skill
    {
       ...
    }
}

You could easily achieve this with the following code:

public function execute(SkillDTO $skillData): Skill
{
    $skill = Skill::create($skillData->attributes());
    $skill->categories()->attach($skillData>categories);
    return $skill;
}

But if you wanted to make it a bit more fluent - akin to using fluent interfaces - you could do the following:

1. Add the Tappable trait to you model:

class Skill extends Model
{
    use \Illuminate\Support\Traits\Tappable;
    ...
}

2. Rewrite the execute method to use the tap method - now present on the model:

public function execute(SkillDTO $skillData): Skill
{
    return Skill::create($skillData->attributes())
        ->tap(fn($skill)=>$skill->categories()->attach($skillData->categories));
}

How to configure path aliases in Jest

If you use webpack aliases for your imports you need to let Jest know about them before you can test your code. You can do this in a jest configuration file - jest.config.js for instance:

module.exports = {
  moduleNameMapper: {
    //an import like: '@/interfaces/Foo.ts' 
    //becomes '[your-root]/src/interfaces/Foo.ts'
    '@/(.*)$': '<rootDir>/src/$1',
  },
  ...
};

If you want to learn how to configure webpack aliases you can read this post

How to trigger model events on pivot tables in Laravel

Suppose we have an app where we have a couple of tasks and users and we want to be able to assign users to tasks.

But what if at the same time we want to know who assigned each user to each task? We can easily achieve this by taking advantage of Laravel's model events - for pivot tables.

We would need 3 tables: one for tasks, one for users and one for the assigning users to tasks:

Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->string("title");
    ...
    $table->timestamps();
});

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string("username");
    ...
    $table->timestamps();
});

Schema::create('task_user', function (Blueprint $table) {
    $table->id();
    $table->foreignId("assignee_id")->constrained("users");
    $table->foreignId("skill_id")->constrained();
    $table->foreignId("user_id")->constrained();
    $table->timestamps();
});

1. Create the corresponding TaskUser pivot model.

 php artisan make:model TaskUser --pivot

2. Add your event handlers.

class TaskUser extends Pivot
{
    protected static function booted()
    {
        static::creating(function ($pivot_model) {
            // your implementation here
        });
        ...
    }
}

3. Let Laravel know which model it should use for the pivot table.

This is the key - without this step the callback described above will not be executed!

//in the User model
public function tasks()
{
    return $this->belongsToMany(Task::class)
      ->using(TaskUser::class);
}

Now, you can do:

$user->tasks()->attach($tasks);

That's it! Your event callbacks or corresponding observer methods should now be executed.

How use Laravel's Bootable Eloquent Traits

Given the following bootable trait:

trait WithCreator
{
    //no need to define a 'boot' method
    public static function bootWithCreator()
    {
        self::observe(CreatorObserver::class);
    }
}
class CreatorObserver
{
    public function creating($model)
    {
        $model->creator()->associate(auth()->user());
    }
}

We can use it to register the observer without colliding with existing boot methods inside the model.

class Comment extends Model
{
    use HasFactory;
    use WithCreator;
    
    public static function boot()
    {
        ...
    }
}

How to use Makefiles to boost command-line productivity

Makefiles are an awesome tool which can help you become more productive command-line wise by encapsulating long commands and/or sequences of commands. They also help abstract away complexity.

One practical example of creating a make command would be setting up a Laravel project. All you need to do is create a file called Makefile and type in the following.

.PHONY setup
setup: # Setup project
    composer install
    php artisan migrate:fresh --seed
    npm install
    ...

Now, a new developer who needs to setup his/her project can skip writing down that series of commands by simply typing in the following:

make setup

But this doesn't end here. You can create virtually any command to help you automate certain processes such as creating or deleting files, changing permissions etc.

How to use factories to create relationships which follow a sequence

Factories an extremely useful tool for testing. The more complex the use case, the more awesome you find out Laravel factories are.

For instance, we might need to create a few items, each one with a different measurement of its own:

 Item::factory()
    //...
    ->has(Measurement::factory()->state(new Sequence(
        ['length' => 50, "width" => 110],
        ['length' => 65, "width" => 200],
        ['length' => 190, "width" => 295],
    )))
  ->count(3)
  ->create()

This will result in 3 items, each item having one unique measurement assigned. The first item will have a measurement with values corresponding to the first array in the sequence and so on.

How to extend interfaces declared in external libraries in Typescript

Typescript allows us to easily extend types by using module augumentation.

Let's take a look at one quick example - extending the React Material Ui Library Theme.

All we need to do is create a file ending in .d.ts at the root of our Typescript project - in this case I'll name it material-ui.d.ts:

import {
  Theme as MuiTheme,
} from '@mui/material/styles';

declare module '@mui/material/styles' {
  export interface Theme extends MuiTheme {
    customization?: Record<string, string>;
  }
}

How to set default attribute values for Laravel models

The problem:

Given the following schema:

 Schema::create('posts', function (Blueprint $table) {
      $table->id();
      $table->unsignedBigInteger('number_of_hits')->default(0);
      $table->string('title');
  });

We when we create a new post:

$post = new Post(["title" => "test"]);

we might expect the 'number_of_hits' to be 0, but it is null.

The solution:

To fix this, we can easily tell Laravel what default values we want for the model attributes:

class Post extends Model
{
   ...
   protected $attributes = [
        'number_of_hits' => 0,
    ];
}

How to declare path aliases in Typescript

Defining path aliases using webpack can save you a lot of headache when it comes to imports, but you must also let Typescript know about them.

Following my previous post on declaring path aliases using webpack, you can configure your tsconfig.json file to in order to be able to use those aliases in Typescript like so:

{
  ...
  "paths": {
    "@/*": ["./src/*"],
    "images/*": ["./assets/images/*"],
  },
  "include": [
    ...
  ],
}

Of course, all paths for defined aliases must be reachable by Typescript. You can check this post out if you are not sure how to do that.

Otherwise, we are going to get this error:

Cannot find module 'images/[your module]' or its corresponding type declarations.

How to import images in Typescript

Normally, when Typescript cannot find something we get this error:

Cannot find module [your module] or its corresponding type declarations ts(2307)

Now let's see how we can fix this, with a simple example. Given the following folder structure:

│── src
│   ├── resources
│   │   ├── ts
│   │   │   ├── **/*.ts
│   │   ├── images
│   │   │   ├── logo.svg
├── ...
├── tsconfig.json

In order to be able to achieve something like this:

import Logo from '[path]/images/logo.svg'

without any ** Typescript errors**, we need to follow these steps:

1.Make sure that the images folder is "reachable" by Typescript.

Adding this inside tsconfig.json will do the trick:

{
  "include": [
        ...
        "resources/**/*.ts",
   ],
}

A configuration like this one will not do:

{
  "include": [
      "resources/ts/**/*.ts",
   ],
}

Because images/ is not included in ts/ and we don't have any other folders declared, so Typescript can't "reach" it.

2.Let Typescript know about the .svg type in that folder.

In the root of the images folder create a file called index.d.ts:

...
├── resources
│   ├── ts
│   │   ├── **/*.ts
│   ├── images
│   │   ├── logo.svg
│   │   ├── index.d.ts
...

With the following contents:

declare module '*.svg' {
  const value: any;
  export = value;
}

Now you should be good to go.

SQLite vs MySQL - foreign key checks

Sqlite does not check for foreign key integrity when creating tables, only when inserting records. However, MySQL does check in both cases.

To make this more clear, let's take a look at a simple example: given two tables - todos and users - created in this exact order:

 Schema::create('todos', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->unsignedBigInteger('user_id');
      $table->foreign('user_id')
          ->references('id')
          ->on('users');
  });
 Schema::create('users', function (Blueprint $table) {
      $table->id();
      $table->string('username');
      $table->string('password');
  });

If you run your migrations using the sqlite driver, everything works just fine.

However, if you run your migrations using the mysql driver, you get the following error:

SQLSTATE[HY000]: General error: 1005 Can't create table `[your-app]`.`todos` 
(errno: 150 "Foreign key constraint is incorrectly formed") 
(SQL: alter table `todos` add constraint `todos_user_id_foreign`
 foreign key (`user_id`) references `users` (`id`))

Folder aliases using webpack

In order to define a folder alias using webpack, all you have to do is write the following in webpack.config.js:

const path = require('path');

module.exports = {
  ...
  resolve: {
    alias: {
      '@': path.resolve('src'),
      images: path.resolve('assets/images'),
    },
  },
};

Now , if you want to import something from assets/images, all you need do to is

import image from 'images/[path]'

regardless of the current directory.

If the location of any of the folders defined with aliases ever changes, you no longer need to update all the imports - changing the path in webpack.config.js will take care of everything.