A Motoko-based contact management system with persistent storage capabilities and an migration path.
Two examples of a migration path for a single canister:
- main Branch example of a migration path for a single canister using a persistent actor
- asCass Branch example of a migration path for a single canister using a persistent actor class
This project provides a simple contact management system built with Motoko on the Internet Computer. It allows you to add, update, retrieve, and list contact records with persistent storage. Additonally we are going to update the concact type with a new property.
Adds a new contact record to the system.
Parameters:
contact
: A ContactAdd object withfirstName
(Text)
Returns:
Nat
: The unique ID assigned to the new contact
CLI Usage:
# Add a new contact
dfx canister call backend addContact '(record { firstName = "John" })'
# Add another contact
dfx canister call backend addContact '(record { firstName = "Jane" })'
Updates an existing contact record by ID.
Parameters:
contact
: A Contact object withid
(Nat) andfirstName
(Text)
Returns:
Bool
:true
if the contact was successfully updated,false
if the contact ID doesn't exist
CLI Usage:
# Update contact with ID 0
dfx canister call backend updateContact '(record { id = 0; firstName = "John Updated" })'
# Update contact with ID 1
dfx canister call backend updateContact '(record { id = 1; firstName = "Jane Updated" })'
Retrieves a specific contact record by ID.
Parameters:
id
: The unique identifier of the contact (Nat)
Returns:
?Contact
: The contact record if found, ornull
if not found
CLI Usage:
# Get contact with ID 0
dfx canister call backend getContact '(0)'
# Get contact with ID 1
dfx canister call backend getContact '(1)'
Retrieves all contact records in the system.
Parameters:
- None
Returns:
[(Nat, Contact)]
: Array of tuples containing (ID, Contact) pairs
CLI Usage:
# Get all contacts
dfx canister call backend getAllContacts '()'
type Contact = {
id : Nat;
firstName : Text;
};
type ContactAdd = {
firstName : Text;
};
- DFX (Internet Computer SDK)
- A local Internet Computer replica or connection to mainnet
- Start the local replica:
dfx start --background
- Deploy the canister:
dfx deploy
- Test the methods:
# Add some contacts
dfx canister call backend addContact '(record { firstName = "Alice" })'
dfx canister call backend addContact '(record { firstName = "Bob" })'
# List all contacts
dfx canister call backend getAllContacts '()'
# Get a specific contact
dfx canister call backend getContact '(0)'
# Update a contact
dfx canister call backend updateContact '(record { id = 0; firstName = "Alice Updated" })'
Here's a complete example of using the contact management system:
# 1. Deploy the canister
dfx deploy
# 2. Add contacts
dfx canister call backend addContact '(record { firstName = "John Doe" })'
# Returns: (0)
dfx canister call backend addContact '(record { firstName = "Jane Smith" })'
# Returns: (1)
# 3. List all contacts
dfx canister call backend getAllContacts '()'
# Returns: vec { (0 : nat, record { id = 0; firstName = "John Doe" }); (1 : nat, record { id = 1; firstName = "Jane Smith" }) }
# 4. Get a specific contact
dfx canister call backend getContact '(0)'
# Returns: (opt record { id = 0; firstName = "John Doe" })
# 5. Update a contact
dfx canister call backend updateContact '(record { id = 0; firstName = "John Updated" })'
# Returns: (true)
# 6. Verify the update
dfx canister call backend getContact '(0)'
# Returns: (opt record { id = 0; firstName = "John Updated" })
- The system automatically assigns unique IDs to new contacts
- When adding contacts, only provide the
firstName
- the ID will be auto-generated - Contact updates require the exact ID of the existing contact
- All data is persisted using Motoko's orthogonal persistence
- The
getAllContacts()
method returns contacts in the order they were added
Now the situation has changed and we want to add a new information to the contact store.
Adding a lastName
field to contacts:
- Old type:
{ id: Nat, firstName: Text }
- New type:
{ id: Nat, firstName: Text, lastName: Text }
- Migration strategy: Set
lastName = ""
for all existing contacts
type Contact = {
id : Nat;
firstName : Text;
lastName : Text;
};
Add new types to the types.mo
file (see file):
- Create
Contact_v1
type with the newlastName
field - Create
ContactAdd_v1
type for adding new contacts - Keep existing types for backward compatibility during migration
Add a new migration file ContactMigration_1.mo
to the migration folder (see file):
- Define the migration function that transforms
Contact
toContact_v1
- Handle the data transformation from old schema to new schema
- Set default values for new required fields
Uncomment the migration lines in main.mo
:
import ContactMigration "./migration/ContactMigration_1";
(with migration = ContactMigration.migration)
Modify the main actor to work with new types:
- Update
addContact
function to useContactAdd_v1
- Update
updateContact
function to useContact_v1
- Ensure all methods handle the new
lastName
field
dfx deploy
- The migration will automatically run during deployment
- Existing data will be transformed to include the new
lastName
field - All existing contacts will have
lastName = ""
After successful migration, comment out the migration lines again:
// import ContactMigration "./migration/ContactMigration_1";
// (with migration = ContactMigration.migration)
Test the migration by checking existing data:
dfx canister call backend getContacts '()'
- Verify all existing contacts now have the
lastName
field - Confirm the field contains an empty string
""
as expected - Test adding new contacts with the
lastName
field
- Backup First: Always backup your data before running migrations
- Test Environment: Test the migration on a development environment first
- Monitor Deployment: Watch for any errors during the migration process
- Verify Data Integrity: Ensure all existing data is preserved and accessible
- Clean Up: Remove migration code after successful completion to keep the codebase clean
After migration to Contact_v1
with lastName
field:
# Add contact with firstName and lastName
dfx canister call backend addContact '(record { firstName = "John"; lastName = "Doe" })'
# Update contact with all fields
dfx canister call backend updateContact '(record { id = 0; firstName = "John"; lastName = "Smith" })'
# Get specific contact
dfx canister call backend getContact '(0)'
# List all contacts
dfx canister call backend getContacts '()'