wekan/METEOR3_MIGRATION.md
2026-02-19 00:26:47 +02:00

5.8 KiB

Meteor 3.0 Migration Guide

Reference document capturing patterns, constraints, and lessons learned during the async migration of WeKan from Meteor 2.16 toward Meteor 3.0 readiness.


1. Dual-Compatibility Strategy

WeKan runs on Meteor 2.16 with Blaze 2.x. The goal is dual compatibility: changes must work on 2.16 now and remain compatible with a future Meteor 3.0 upgrade.

Key constraint: Blaze 2.x does NOT support async template helpers. Client-side code must receive synchronous data.


2. ReactiveCache Facade Pattern

ReactiveCache dispatches to ReactiveCacheServer (async MongoDB) or ReactiveCacheClient (sync Minimongo).

Rule: Facade methods must NOT be async. They return a Promise on the server and data on the client. Server callers await; client code uses the return value directly.

// CORRECT:
getBoard(boardId) {
  if (Meteor.isServer) {
    return ReactiveCacheServer.getBoard(boardId);  // Returns Promise
  } else {
    return ReactiveCacheClient.getBoard(boardId);  // Returns data
  }
}

// WRONG:
async getBoard(boardId) { ... }  // Wraps client return in Promise too!

3. Model Helpers (Collection.helpers)

Model helpers defined via Collection.helpers({}) are used by Blaze templates. They must NOT be async.

// CORRECT:
Cards.helpers({
  board() {
    return ReactiveCache.getBoard(this.boardId);  // Promise on server, data on client
  },
});

// WRONG:
Cards.helpers({
  async board() {  // Blaze gets Promise instead of data
    return await ReactiveCache.getBoard(this.boardId);
  },
});

Server-side callers of these helpers must await the result:

// In a Meteor method or hook (server-only):
const board = await card.board();

4. Allow/Deny Callbacks Must Be Synchronous

Meteor 2.x evaluates allow/deny callbacks synchronously. An async callback returns a Promise:

  • allow callback returning Promise (truthy) → always passes
  • deny callback returning Promise (truthy) → always denies

Rule: Never use async in allow/deny. Replace ReactiveCache calls with direct sync Mongo calls.

// CORRECT:
Cards.allow({
  insert(userId, doc) {
    return allowIsBoardMemberWithWriteAccess(userId, Boards.findOne(doc.boardId));
  },
  fetch: ['boardId'],
});

// WRONG:
Cards.allow({
  async insert(userId, doc) {
    return allowIsBoardMemberWithWriteAccess(userId, await ReactiveCache.getBoard(doc.boardId));
  },
});

Sync alternatives for common patterns:

Async (broken in allow/deny) Sync replacement
await ReactiveCache.getBoard(id) Boards.findOne(id)
await ReactiveCache.getCard(id) Cards.findOne(id)
await ReactiveCache.getCurrentUser() Meteor.users.findOne(userId)
await ReactiveCache.getBoards({...}) Boards.find({...}).fetch()
await card.board() Boards.findOne(card.boardId)

Note: These sync Mongo calls (findOne, find().fetch()) are available in Meteor 2.x. In Meteor 3.0, they will be replaced by findOneAsync / find().fetchAsync(), which will require allow/deny callbacks to be reworked again (or replaced by Meteor 3.0's new permission model).


5. Server-Only Code CAN Be Async

Code that runs exclusively on the server can safely use async/await:

  • Meteor.methods({}) — method bodies
  • Meteor.publish() — publication functions
  • JsonRoutes.add() — REST API handlers
  • Collection.before.* / Collection.after.* — collection hooks (via matb33:collection-hooks)
  • Standalone server functions
Meteor.methods({
  async createCard(data) {
    const board = await ReactiveCache.getBoard(data.boardId);  // OK
    // ...
  },
});

6. forEach with await Anti-Pattern

Array.forEach() does not handle async callbacks — iterations run concurrently without awaiting.

// WRONG:
items.forEach(async (item) => {
  await processItem(item);  // Runs all in parallel, not sequentially
});

// CORRECT:
for (const item of items) {
  await processItem(item);  // Runs sequentially
}

7. Client-Side Collection Updates

Meteor requires client-side collection updates to use _id as the selector:

// CORRECT:
Lists.updateAsync(listId, { $set: { title: newTitle } });

// WRONG - fails with "Untrusted code may only update documents by ID":
Lists.updateAsync({ _id: listId, boardId: boardId }, { $set: { title: newTitle } });

8. Sync Meteor 2.x APIs to Convert for 3.0

These Meteor 2.x sync APIs will need conversion when upgrading to Meteor 3.0:

Meteor 2.x (sync) Meteor 3.0 (async)
Collection.findOne() Collection.findOneAsync()
Collection.find().fetch() Collection.find().fetchAsync()
Collection.insert() Collection.insertAsync()
Collection.update() Collection.updateAsync()
Collection.remove() Collection.removeAsync()
Collection.upsert() Collection.upsertAsync()
Meteor.user() Meteor.userAsync()
Meteor.userId() Remains sync

Current status: Server-side code already uses async patterns via ReactiveCache. The sync findOne() calls in allow/deny callbacks will need to be addressed when Meteor 3.0's allow/deny system supports async (or is replaced).


9. Files Reference

Key files involved in the async migration:

File Role
imports/reactiveCache.js ReactiveCache facade + Server/Client/Index implementations
server/lib/utils.js Permission helper functions (allowIsBoardMember*)
models/*.js Collection schemas, helpers, allow/deny, hooks, methods
server/publications/*.js Meteor publications
server/rulesHelper.js Rule trigger/action evaluation
server/cronMigrationManager.js Cron-based migration jobs