Today is ∞/∞/2^3 - Infinity Day - Origyn has 3 Gifts for the Motoko Community!
Gift 1: A migration pattern
One of our developers, @ZhenyaUsenko, came up with this great migration pattern.
This pattern allows users to use stable variables and keep a record of migrations in their code.
Your main actor looks like this:
import Array "mo:base/Array";
import Types "./types";
import Migrations "./migrations";
import MigrationTypes "./migrations/types";
shared deployer actor class MotokoMigrations() {
let StateTypes = MigrationTypes.Current;
// you will have only one stable variable
// move all your stable variable declarations to "migrations/001-initial/types.mo -> State"
stable var migrationState: MigrationTypes.State = #state000(#data);
// do not forget to change #state002 when you are adding a new migration
// if you use one previous states in place of #state002 it will run downgrade methods instead
migrationState := Migrations.migrate(migrationState, #state002(#id), { deployer = deployer.caller });
// do not forget to change #state002 when you are adding a new migration
let #state002(#data(state)) = migrationState;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public shared func addTeacher(teacher: StateTypes.Teacher): async () {
state.teachers := Array.append(state.teachers, [teacher]);
};
public shared func addStudent(student: StateTypes.Student): async () {
state.students := Array.append(state.students, [student]);
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public query func fetchTeachers(): async Types.FetchTeachersResponse {
return {
items = state.teachers;
totalCount = state.teachers.size();
};
};
public query func fetchStudents(): async Types.FetchStudentsResponse {
return {
items = state.students;
totalCount = state.students.size();
};
};
};
Your migrations are held in a migrations directory that looks like:
And each migration has a lib.mo:
import Array "mo:base/Array";
import Types001 "../001-initial/types";
import Types002 "./types";
import MigrationTypes "../types";
module {
public func upgrade(prevMigrationState: MigrationTypes.State, args: MigrationTypes.Args): MigrationTypes.State {
// access previous state
let #state001(#data(prevState)) = prevMigrationState;
// make any manipulations with previous state to convert it to current migration state type
let teachers = Array.map(prevState.teachers, func (item: Types001.Teacher): Types002.Teacher {
return {
firstName = item.firstName;
lastName = item.lastName;
fullName = item.firstName # " " # item.lastName;
subject = item.subject;
};
});
let students = Array.map(prevState.students, func (item: Types001.Student): Types002.Student {
return {
firstName = item.firstName;
lastName = item.lastName;
fullName = item.firstName # " " # item.lastName;
speciality = item.speciality;
};
});
// return current state
return #state002(#data({
var admin = prevState.admin;
var teachers;
var students;
}));
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public func downgrade(migrationState: MigrationTypes.State, args: MigrationTypes.Args): MigrationTypes.State {
// access current state
let #state002(#data(state)) = migrationState;
// make any manipulations with current state to convert it to previous migration state type
let teachers = Array.map(state.teachers, func (item: Types002.Teacher): Types001.Teacher {
return {
firstName = item.firstName;
lastName = item.lastName;
subject = item.subject;
};
});
let students = Array.map(state.students, func (item: Types002.Student): Types001.Student {
return {
firstName = item.firstName;
lastName = item.lastName;
speciality = item.speciality;
};
});
// return previous state
return #state001(#data({
var admin = state.admin;
var teachers;
var students;
}));
// if you are sure you wont need downgrades in your project, you can just "return #state000(#data);"
// note that it will fail to deploy if you then try to downgrade
};
};
and a types.moc:
// please do not import any types from your project outside migrations folder here
// it can lead to bugs when you change those types later, because migration types should not be changed
// you should also avoid importing these types anywhere in your project directly from here
// use MigrationTypes.Current property instead
module {
public type Teacher = {
firstName: Text;
lastName: Text;
fullName: Text;
subject: Text;
};
public type Student = {
firstName: Text;
lastName: Text;
fullName: Text;
speciality: Text;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public type State = {
// this is the data you previously had as stable variables inside your actor class
var admin: Principal;
var teachers: [Teacher];
var students: [Student];
};
};
Please explore the pattern, ask questions, and test that it works for you. It should simplify changing data during upgrades.