From 5c0d6835a809b95197db0ef072862038b637e32a Mon Sep 17 00:00:00 2001 From: denihs Date: Thu, 14 Nov 2024 10:38:44 -0400 Subject: [PATCH 01/32] - create Roles document --- packages/roles/roles_common_async.js | 462 ++++++-------- v3-docs/docs/.vitepress/config.mts | 904 ++++++++++++++------------- v3-docs/docs/api/roles.md | 452 ++++++++++++++ 3 files changed, 1086 insertions(+), 732 deletions(-) create mode 100644 v3-docs/docs/api/roles.md diff --git a/packages/roles/roles_common_async.js b/packages/roles/roles_common_async.js index 11079194d5..a78281db83 100644 --- a/packages/roles/roles_common_async.js +++ b/packages/roles/roles_common_async.js @@ -24,7 +24,6 @@ import { Mongo } from 'meteor/mongo' * - `scope`: scope name * - `inheritedRoles`: A list of all the roles objects inherited by the assigned role. * - * @module Roles */ export const RolesCollection = new Mongo.Collection('roles') @@ -39,7 +38,7 @@ if (!Meteor.roleAssignment) { } /** - * @class Roles + * class Roles */ if (typeof Roles === 'undefined') { Roles = {} // eslint-disable-line no-global-assign @@ -60,6 +59,10 @@ const asyncSome = async (arr, predicate) => { return false } +/** + * @namespace Roles + * @summary The namespace for all Roles types and methods. + */ Object.assign(Roles, { /** * Used as a global group (now scope) name. Not used anymore. @@ -71,14 +74,13 @@ Object.assign(Roles, { GLOBAL_GROUP: null, /** - * Create a new role. - * - * @method createRoleAsync + * @summary Create a new role. + * @memberof Roles + * @locus Anywhere * @param {String} roleName Name of role. * @param {Object} [options] Options: - * - `unlessExists`: if `true`, exception will not be thrown in the role already exists - * @return {Promise} ID of the new role or null. - * @static + * - `unlessExists`: if `true`, exception will not be thrown in the role already exists + * @return {Promise} ID of the new role or null */ createRoleAsync: async function (roleName, options) { Roles._checkRoleName(roleName) @@ -116,14 +118,12 @@ Object.assign(Roles, { }, /** - * Delete an existing role. - * + * @summary Delete an existing role. * If the role is set for any user, it is automatically unset. - * - * @method deleteRoleAsync + * @memberof Roles + * @locus Anywhere * @param {String} roleName Name of role. * @returns {Promise} - * @static */ deleteRoleAsync: async function (roleName) { let roles @@ -182,13 +182,12 @@ Object.assign(Roles, { }, /** - * Rename an existing role. - * - * @method renameRoleAsync + * @summary Rename an existing role. + * @memberof Roles + * @locus Anywhere * @param {String} oldName Old name of a role. * @param {String} newName New name of a role. * @returns {Promise} - * @static */ renameRoleAsync: async function (oldName, newName) { let count @@ -254,16 +253,12 @@ Object.assign(Roles, { }, /** - * Add role parent to roles. - * - * Previous parents are kept (role can have multiple parents). For users which have the - * parent role set, new subroles are added automatically. - * - * @method addRolesToParentAsync + * @summary Add role parent to roles. + * @memberof Roles + * @locus Anywhere * @param {Array|String} rolesNames Name(s) of role(s). * @param {String} parentName Name of parent role. * @returns {Promise} - * @static */ addRolesToParentAsync: async function (rolesNames, parentName) { // ensure arrays @@ -339,16 +334,14 @@ Object.assign(Roles, { }, /** - * Remove role parent from roles. - * + * @summary Remove role parent from roles. * Other parents are kept (role can have multiple parents). For users which have the * parent role set, removed subrole is removed automatically. - * - * @method removeRolesFromParentAsync - * @param {Array|String} rolesNames Name(s) of role(s). - * @param {String} parentName Name of parent role. - * @returns {Promise} - * @static + * @memberof Roles + * @locus Anywhere + @param {Array|String} rolesNames Name(s) of role(s). + @param {String} parentName Name of parent role. + @returns {Promise} */ removeRolesFromParentAsync: async function (rolesNames, parentName) { // ensure arrays @@ -431,26 +424,16 @@ Object.assign(Roles, { }, /** - * Add users to roles. - * + * @summary Add users to roles. * Adds roles to existing roles for each user. - * - * @example - * Roles.addUsersToRolesAsync(userId, 'admin') - * Roles.addUsersToRolesAsync(userId, ['view-secrets'], 'example.com') - * Roles.addUsersToRolesAsync([user1, user2], ['user','editor']) - * Roles.addUsersToRolesAsync([user1, user2], ['glorious-admin', 'perform-action'], 'example.org') - * - * @method addUsersToRolesAsync + * @memberof Roles + * @locus Anywhere * @param {Array|String} users User ID(s) or object(s) with an `_id` field. * @param {Array|String} roles Name(s) of roles to add users to. Roles have to exist. * @param {Object|String} [options] Options: * - `scope`: name of the scope, or `null` for the global role * - `ifExists`: if `true`, do not throw an exception if the role does not exist * @returns {Promise} - * - * Alternatively, it can be a scope name string. - * @static */ addUsersToRolesAsync: async function (users, roles, options) { let id @@ -487,17 +470,10 @@ Object.assign(Roles, { }, /** - * Set users' roles. - * + * @summary Set users' roles. * Replaces all existing roles with a new set of roles. - * - * @example - * await Roles.setUserRolesAsync(userId, 'admin') - * await Roles.setUserRolesAsync(userId, ['view-secrets'], 'example.com') - * await Roles.setUserRolesAsync([user1, user2], ['user','editor']) - * await Roles.setUserRolesAsync([user1, user2], ['glorious-admin', 'perform-action'], 'example.org') - * - * @method setUserRolesAsync + * @memberof Roles + * @locus Anywhere * @param {Array|String} users User ID(s) or object(s) with an `_id` field. * @param {Array|String} roles Name(s) of roles to add users to. Roles have to exist. * @param {Object|String} [options] Options: @@ -505,9 +481,6 @@ Object.assign(Roles, { * - `anyScope`: if `true`, remove all roles the user has, of any scope, if `false`, only the one in the same scope * - `ifExists`: if `true`, do not throw an exception if the role does not exist * @returns {Promise} - * - * Alternatively, it can be a scope name string. - * @static */ setUserRolesAsync: async function (users, roles, options) { let id @@ -714,23 +687,15 @@ Object.assign(Roles, { }, /** - * Remove users from assigned roles. - * - * @example - * await Roles.removeUsersFromRolesAsync(userId, 'admin') - * await Roles.removeUsersFromRolesAsync([userId, user2], ['editor']) - * await Roles.removeUsersFromRolesAsync(userId, ['user'], 'group1') - * - * @method removeUsersFromRolesAsync + * @summary Remove users from assigned roles. + * @memberof Roles + * @locus Anywhere * @param {Array|String} users User ID(s) or object(s) with an `_id` field. * @param {Array|String} roles Name(s) of roles to remove users from. Roles have to exist. * @param {Object|String} [options] Options: * - `scope`: name of the scope, or `null` for the global role * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) * @returns {Promise} - * - * Alternatively, it can be a scope name string. - * @static */ removeUsersFromRolesAsync: async function (users, roles, options) { if (!users) throw new Error("Missing 'users' param.") @@ -792,21 +757,9 @@ Object.assign(Roles, { }, /** - * Check if user has specified roles. - * - * @example - * // global roles - * await Roles.userIsInRoleAsync(user, 'admin') - * await Roles.userIsInRoleAsync(user, ['admin','editor']) - * await Roles.userIsInRoleAsync(userId, 'admin') - * await Roles.userIsInRoleAsync(userId, ['admin','editor']) - * - * // scope roles (global roles are still checked) - * await Roles.userIsInRoleAsync(user, 'admin', 'group1') - * await Roles.userIsInRoleAsync(userId, ['admin','editor'], 'group1') - * await Roles.userIsInRoleAsync(userId, ['admin','editor'], {scope: 'group1'}) - * - * @method userIsInRoleAsync + * @summary Check if user has specified roles. + * @memberof Roles + * @locus Anywhere * @param {String|Object} user User ID or an actual user object. * @param {Array|String} roles Name of role or an array of roles to check against. If array, * will return `true` if user is in _any_ role. @@ -818,7 +771,6 @@ Object.assign(Roles, { * * Alternatively, it can be a scope name string. * @return {Promise} `true` if user is in _any_ of the target roles - * @static */ userIsInRoleAsync: async function (user, roles, options) { let id @@ -869,9 +821,9 @@ Object.assign(Roles, { }, /** - * Retrieve user's roles. - * - * @method getRolesForUserAsync + * @summary Retrieve user's roles. + * @memberof Roles + * @locus Anywhere * @param {String|Object} user User ID or an actual user object. * @param {Object|String} [options] Options: * - `scope`: name of scope to provide roles for; if not specified, global roles are returned @@ -884,7 +836,6 @@ Object.assign(Roles, { * * Alternatively, it can be a scope name string. * @return {Promise} Array of user's roles, unsorted. - * @static */ getRolesForUserAsync: async function (user, options) { let id @@ -954,13 +905,133 @@ Object.assign(Roles, { }, /** - * Retrieve cursor of all existing roles. - * - * @method getAllRoles - * @param {Object} [queryOptions] Options which are passed directly - * through to `RolesCollection.find(query, options)`. + * @summary Retrieve users scopes, if any. + * @memberof Roles + * @locus Anywhere + * @param {String|Object} user User ID or an actual user object. + * @param {Array|String} [roles] Name of roles to restrict scopes to. + * @return {Promise} Array of user's scopes, unsorted. + */ + getScopesForUserAsync: async function (user, roles) { + let id + + if (roles && !Array.isArray(roles)) roles = [roles] + + if (user && typeof user === 'object') { + id = user._id + } else { + id = user + } + + if (!id) return [] + + const selector = { + 'user._id': id, + scope: { $ne: null } + } + + if (roles) { + selector['inheritedRoles._id'] = { $in: roles } + } + + const scopes = ( + await Meteor.roleAssignment + .find(selector, { fields: { scope: 1 } }) + .fetchAsync() + ).map((obi) => obi.scope) + + return [...new Set(scopes)] + }, + + /** + * @summary Rename a scope. + * @memberof Roles + * @locus Anywhere + * @param {String} oldName Old name of a scope. + * @param {String} newName New name of a scope. + * @returns {Promise} + */ + renameScopeAsync: async function (oldName, newName) { + let count + + Roles._checkScopeName(oldName) + Roles._checkScopeName(newName) + + if (oldName === newName) return + + do { + count = await Meteor.roleAssignment.updateAsync( + { + scope: oldName + }, + { + $set: { + scope: newName + } + }, + { multi: true } + ) + } while (count > 0) + }, + + /** + * @summary Remove a scope and all its role assignments. + * @memberof Roles + * @locus Anywhere + * @param {String} name The name of a scope. + * @returns {Promise} + */ + removeScopeAsync: async function (name) { + Roles._checkScopeName(name) + + await Meteor.roleAssignment.removeAsync({ scope: name }) + }, + + /** + * @summary Find out if a role is an ancestor of another role. + * @memberof Roles + * @locus Anywhere + * @param {String} parentRoleName The role you want to research. + * @param {String} childRoleName The role you expect to be among the children of parentRoleName. + * @returns {Promise} True if parent role is ancestor of child role + */ + isParentOfAsync: async function (parentRoleName, childRoleName) { + if (parentRoleName === childRoleName) { + return true + } + + if (parentRoleName == null || childRoleName == null) { + return false + } + + Roles._checkRoleName(parentRoleName) + Roles._checkRoleName(childRoleName) + + let rolesToCheck = [parentRoleName] + while (rolesToCheck.length !== 0) { + const roleName = rolesToCheck.pop() + + if (roleName === childRoleName) { + return true + } + + const role = await Meteor.roles.findOneAsync({ _id: roleName }) + + // This should not happen, but this is a problem to address at some other time. + if (!role) continue + + rolesToCheck = rolesToCheck.concat(role.children.map((r) => r._id)) + } + + return false + }, + + /** + * @summary Retrieve cursor of all existing roles. + * @memberof Roles + * @locus Anywhere + * @param {Object} [queryOptions] Options which are passed directly through to `Meteor.roles.find(query, options)`. * @return {Cursor} Cursor of existing roles. - * @static */ getAllRoles: function (queryOptions) { queryOptions = queryOptions || { sort: { _id: 1 } } @@ -969,28 +1040,16 @@ Object.assign(Roles, { }, /** - * Retrieve all users who are in target role. - * - * Options: - * - * @method getUsersInRoleAsync - * @param {Array|String} roles Name of role or an array of roles. If array, users - * returned will have at least one of the roles - * specified but need not have _all_ roles. - * Roles do not have to exist. + * @summary Retrieve all users who are in target role. + * @memberof Roles + * @locus Anywhere + * @param {Array|String} roles Name of role or an array of roles. * @param {Object|String} [options] Options: - * - `scope`: name of the scope to restrict roles to; user's global - * roles will also be checked - * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * - `scope`: name of the scope to restrict roles to + * - `anyScope`: if set, role can be in any scope * - `onlyScoped`: if set, only roles in the specified scope are returned - * - `queryOptions`: options which are passed directly - * through to `Meteor.users.find(query, options)` - * - * Alternatively, it can be a scope name string. - * @param {Object} [queryOptions] Options which are passed directly - * through to `Meteor.users.find(query, options)` + * - `queryOptions`: options which are passed directly through to `Meteor.users.find(query, options)` * @return {Promise} Cursor of users in roles. - * @static */ getUsersInRoleAsync: async function (roles, options, queryOptions) { const ids = ( @@ -1004,25 +1063,15 @@ Object.assign(Roles, { }, /** - * Retrieve all assignments of a user which are for the target role. - * - * Options: - * - * @method getUserAssignmentsForRole - * @param {Array|String} roles Name of role or an array of roles. If array, users - * returned will have at least one of the roles - * specified but need not have _all_ roles. - * Roles do not have to exist. + * @summary Retrieve all assignments of a user which are for the target role. + * @memberof Roles + * @locus Anywhere + * @param {Array|String} roles Name of role or an array of roles. * @param {Object|String} [options] Options: - * - `scope`: name of the scope to restrict roles to; user's global - * roles will also be checked - * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) - * - `queryOptions`: options which are passed directly - * through to `RoleAssignmentCollection.find(query, options)` - - * Alternatively, it can be a scope name string. + * - `scope`: name of the scope to restrict roles to + * - `anyScope`: if set, role can be in any scope + * - `queryOptions`: options which are passed directly through to `RoleAssignmentCollection.find(query, options)` * @return {Cursor} Cursor of user assignments for roles. - * @static */ getUserAssignmentsForRole: function (roles, options) { options = Roles._normalizeOptions(options) @@ -1114,157 +1163,6 @@ Object.assign(Roles, { return await Roles.getScopesForUser(...args) }, - /** - * Retrieve users scopes, if any. - * - * @method getScopesForUserAsync - * @param {String|Object} user User ID or an actual user object. - * @param {Array|String} [roles] Name of roles to restrict scopes to. - * - * @return {Promise} Array of user's scopes, unsorted. - * @static - */ - getScopesForUserAsync: async function (user, roles) { - let id - - if (roles && !Array.isArray(roles)) roles = [roles] - - if (user && typeof user === 'object') { - id = user._id - } else { - id = user - } - - if (!id) return [] - - const selector = { - 'user._id': id, - scope: { $ne: null } - } - - if (roles) { - selector['inheritedRoles._id'] = { $in: roles } - } - - const scopes = ( - await Meteor.roleAssignment - .find(selector, { fields: { scope: 1 } }) - .fetchAsync() - ).map((obi) => obi.scope) - - return [...new Set(scopes)] - }, - - /** - * Rename a scope. - * - * Roles assigned with a given scope are changed to be under the new scope. - * - * @method renameScopeAsync - * @param {String} oldName Old name of a scope. - * @param {String} newName New name of a scope. - * @returns {Promise} - * @static - */ - renameScopeAsync: async function (oldName, newName) { - let count - - Roles._checkScopeName(oldName) - Roles._checkScopeName(newName) - - if (oldName === newName) return - - do { - count = await Meteor.roleAssignment.updateAsync( - { - scope: oldName - }, - { - $set: { - scope: newName - } - }, - { multi: true } - ) - } while (count > 0) - }, - - /** - * Remove a scope. - * - * Roles assigned with a given scope are removed. - * - * @method removeScopeAsync - * @param {String} name The name of a scope. - * @returns {Promise} - * @static - */ - removeScopeAsync: async function (name) { - Roles._checkScopeName(name) - - await Meteor.roleAssignment.removeAsync({ scope: name }) - }, - - /** - * Throw an exception if `roleName` is an invalid role name. - * - * @method _checkRoleName - * @param {String} roleName A role name to match against. - * @private - * @static - */ - _checkRoleName: function (roleName) { - if ( - !roleName || - typeof roleName !== 'string' || - roleName.trim() !== roleName - ) { - throw new Error(`Invalid role name '${roleName}'.`) - } - }, - - /** - * Find out if a role is an ancestor of another role. - * - * WARNING: If you check this on the client, please make sure all roles are published. - * - * @method isParentOfAsync - * @param {String} parentRoleName The role you want to research. - * @param {String} childRoleName The role you expect to be among the children of parentRoleName. - * @returns {Promise} - * @static - */ - isParentOfAsync: async function (parentRoleName, childRoleName) { - if (parentRoleName === childRoleName) { - return true - } - - if (parentRoleName == null || childRoleName == null) { - return false - } - - Roles._checkRoleName(parentRoleName) - Roles._checkRoleName(childRoleName) - - let rolesToCheck = [parentRoleName] - while (rolesToCheck.length !== 0) { - const roleName = rolesToCheck.pop() - - if (roleName === childRoleName) { - return true - } - - const role = await Meteor.roles.findOneAsync({ _id: roleName }) - - // This should not happen, but this is a problem to address at some other time. - if (!role) continue - - rolesToCheck = rolesToCheck.concat(role.children.map((r) => r._id)) - } - - return false - }, - /** * Normalize options. * diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 653f1b3b21..5106d81b0e 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -1,450 +1,454 @@ -import { defineConfig } from "vitepress"; - -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "Docs", - description: "Meteor.js Docs", - head: [ - ["link", { rel: "icon", href: "/logo.png" }], - [ - "script", - { - async: "", - src: "https://widget.kapa.ai/kapa-widget.bundle.js", - "data-website-id": "64051b0e-d79f-4fe7-b3ca-ff5c84075693", - "data-project-name": "Meteor", - "data-project-color": "#36436b", - "data-project-logo": "https://v3-docs.meteor.com/logo.png", - "data-modal-disclaimer": - "This is a custom LLM for answering questions about Meteor. Answers are based on the contents of the docs, answered forum posts, YouTube videos and GitHub issues. Please note that answers are generated by AI and may not be fully accurate, so please use your best judgement.", - }, - ], - ], - lastUpdated: true, - sitemap: { - hostname: "https://v3-docs.meteor.com", - }, - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { - text: "Docs", - activeMatch: `^/(guide|docs|examples)/`, - items: [ - { text: "Quick Start", link: "/about/install" }, - { text: "Examples", link: "https://github.com/meteor/examples" }, - { - text: "Meteor.js 2 Docs", - link: "https://v2-docs.meteor.com", - }, - { - text: "Migration from Meteor.js 2", - link: "https://v3-migration-docs.meteor.com", - }, - { - text: "Tutorials", - items: [ - { - text: "Meteor.js 3 + React", - link: "/tutorials/react/index", - }, - { - text: "Meteor + Vue + vue-meteor-tracker", - link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", - }, - ], - }, - ], - }, - { - text: "Ecosystem", - activeMatch: `^/ecosystem/`, - items: [ - { - text: "Community & Help", - items: [ - { - text: "Meteor Forums", - link: "https://forums.meteor.com", - }, - { - text: "Meteor Lounge Discord", - link: "https://discord.gg/hZkTCaVjmT", - }, - { - text: "GitHub Discussions", - link: "https://github.com/meteor/meteor/discussions", - }, - ], - }, - { - text: "Resources", - items: [ - { - text: "Packages on Atmosphere", - link: "https://atmospherejs.com/", - }, - { - text: "VS Code Extension", - link: "https://marketplace.visualstudio.com/items?itemName=meteor-toolbox.meteor-toolbox", - }, - { - text: "DevTools - Chrome Extension", - link: "https://chromewebstore.google.com/detail/ibniinmoafhgbifjojidlagmggecmpgf", - }, - { - text: "DevTools - Firefox Extension", - link: "https://addons.mozilla.org/en-US/firefox/addon/meteor-devtools-evolved/", - }, - ], - }, - { - text: "Learning", - items: [ - { - text: "Meteor University", - link: "https://university.meteor.com", - }, - { - text: "Youtube Channel", - link: "https://www.youtube.com/@meteorsoftware", - }, - ], - }, - { - text: "News", - items: [ - { text: "Blog on Dev.to", link: "https://dev.to/meteor" }, - { text: "Blog on Medium", link: "https://blog.meteor.com" }, - { text: "Twitter", link: "https://x.com/meteorjs" }, - { - text: "LinkedIn", - link: "https://www.linkedin.com/company/meteor-software/", - }, - ], - }, - ], - }, - { text: "API", link: "/api/" }, - { text: "Galaxy Cloud", link: "https://www.meteor.com/cloud" }, - ], - sidebar: [ - { - text: "About", - link: "/about/what-is", - items: [ - { - text: "What is Meteor?", - link: "/about/what-is#introduction", - }, - { - text: "Meteor resources", - link: "/about/what-is#learning-more", - }, - { - text: "Roadmap", - link: "/about/roadmap", - }, - ], - collapsed: true, - }, - { - text: "Quick Start", - items: [ - { - text: "Install Meteor", - link: "/about/install", - }, - { - text: "Web Apps", - link: "/about/web-apps", - }, - { - text: "Cordova", - link: "/about/cordova", - }, - ], - collapsed: true, - }, - { - text: "API", - link: "/api/", - items: [ - { - text: "Accounts", - link: "/api/accounts", - items: [ - { text: "Accounts-Base", link: "/api/accounts#accounts-base" }, - { text: "Multi-server", link: "/api/accounts#multi-server" }, - { text: "Passwords", link: "/api/accounts#passwords" }, - ], - collapsed: true, - }, - { - text: "Meteor", - link: "/api/meteor", - items: [ - { text: "Core", link: "/api/meteor#core" }, - { text: "Methods", link: "/api/meteor#methods" }, - { text: "Publish and Subscribe", link: "/api/meteor#pubsub" }, - { text: "Server connections", link: "/api/meteor#connections" }, - { text: "Timers", link: "/api/meteor#timers" }, - ], - }, - { - text: "Collections", - link: "/api/collections", - }, - { - text: "DDPRateLimiter", - link: "/api/DDPRateLimiter", - }, - { - text: "Check", - link: "/api/check", - }, - { - text: "Session", - link: "/api/session", - }, - { - text: "Blaze", - link: "/api/blaze", - }, - { - text: "Templates", - link: "/api/templates", - }, - { - text: "Email", - link: "/api/email", - }, - { - text: "Tracker", - link: "/api/Tracker", - }, - { - text: "Reactive Var", - link: "/api/ReactiveVar", - }, - { - text: "Reactive Dict", - link: "/api/ReactiveDict", - }, - { - text: "EJSON", - link: "/api/EJSON", - }, - { - text: "Assets", - link: "/api/assets", - }, - { - text: "Mobile Configuration", - link: "/api/app", - }, - { - text: "Package.js", - link: "/api/package", - }, - { - text: "Top Level Await", - link: "/api/top-level-await", - }, - ], - collapsed: true, - }, - { - text: "Packages", - items: [ - { - text: "accounts-ui", - link: "/packages/accounts-ui", - }, - { - text: "accounts-passwordless", - link: "/packages/accounts-passwordless", - }, - { - text: "accounts-2fa", - link: "/packages/accounts-2fa", - }, - { - text: "appcache", - link: "/packages/appcache", - }, - { - text: "audit-arguments-checks", - link: "/packages/audit-argument-checks", - }, - { - text: "autoupdate", - link: "/packages/autoupdate", - }, - { - text: "browser-policy", - link: "/packages/browser-policy", - }, - { - text: "bundler-visualizer", - link: "/packages/bundle-visualizer", - }, - { - text: "coffeescript", - link: "/packages/coffeescript", - }, - { - text: "ecmascript", - link: "/packages/ecmascript", - }, - { - text: "fetch", - link: "/packages/fetch", - }, - { - text: "hot-module-replacement", - link: "/packages/hot-module-replacement", - }, - { - text: "less", - link: "/packages/less", - }, - { - text: "logging", - link: "/packages/logging", - }, - { - text: "markdown", - link: "/packages/markdown", - }, - { - text: "modules", - link: "/packages/modules", - }, - { - text: "oauth-encryption", - link: "/packages/oauth-encryption", - }, - { - text: "random", - link: "/packages/random", - }, - { - text: "server-render", - link: "/packages/server-render", - }, - { - text: "standard-minifier-css", - link: "/packages/standard-minifier-css", - }, - { - text: "underscore", - link: "/packages/underscore", - }, - { - text: "url", - link: "/packages/url", - }, - { - text: "webapp", - link: "/packages/webapp", - }, - { - link: "/packages/packages-listing", - text: "Maintained Packages", - }, - { - link: "packages/community-packages", - text: "Community Packages", - }, - ], - collapsed: true, - }, - { - text: "Troubleshooting", - items: [ - { - text: "Expired Certificates", - link: "/troubleshooting/expired-certificate", - }, - { text: "Windows", link: "/troubleshooting/windows" }, - { - text: "Known issues in 2.13", - link: "/troubleshooting/known-issues", - }, - ], - collapsed: true, - }, - { - text: "Command Line", - items: [ - { link: "/cli/", text: "CLI" }, - { link: "/cli/using-core-types", text: "Using Core Types" }, - { link: "/cli/environment-variables", text: "Environment Variables" }, - ], - collapsed: true, - }, - { - text: "Tutorials", - items: [ - { - text: "Meteor.js 3 + React", - link: "/tutorials/react/index", - }, - { - link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", - text: "Meteor + Vue + vue-meteor-tracker", - }, - ], - collapsed: true, - }, - { - text: "Changelog", - items: [ - // TODO: Open issue in Vitepress about this - { link: "/history", text: "Meteor.js v3 (Current)" }, - { - link: "https://v2-docs.meteor.com/changelog", - text: "Meteor.js v2", - }, - { - link: "https://v2-docs.meteor.com/changelog#v112220211012", - text: "Meteor.js v1", - }, - ], - collapsed: true, - }, - ], - - socialLinks: [ - { icon: "github", link: "https://github.com/meteor/meteor" }, - { icon: "twitter", link: "https://x.com/meteorjs" }, - { icon: "discord", link: "https://discord.gg/hZkTCaVjmT" }, - ], - - logo: { dark: "/meteor-logo.png", light: "/meteor-blue.png" }, - - search: { - provider: "algolia", - options: { - appId: "2RBX3PR26I", - apiKey: "7fcba92008b84946f04369df2afa1744", - indexName: "meteor_docs_v3", - searchParameters: { - facetFilters: ["lang:en"], - }, - }, - }, - - footer: { - message: - 'Released under the MIT License.', - copyright: - 'Copyright (c) 2011 - present Meteor Software.', - }, - editLink: { - pattern: "https://github.com/meteor/meteor/edit/devel/v3-docs/docs/:path", - text: "Edit this page on GitHub", - }, - }, -}); +import { defineConfig } from "vitepress"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Docs", + description: "Meteor.js Docs", + head: [ + ["link", { rel: "icon", href: "/logo.png" }], + [ + "script", + { + async: "", + src: "https://widget.kapa.ai/kapa-widget.bundle.js", + "data-website-id": "64051b0e-d79f-4fe7-b3ca-ff5c84075693", + "data-project-name": "Meteor", + "data-project-color": "#36436b", + "data-project-logo": "https://v3-docs.meteor.com/logo.png", + "data-modal-disclaimer": + "This is a custom LLM for answering questions about Meteor. Answers are based on the contents of the docs, answered forum posts, YouTube videos and GitHub issues. Please note that answers are generated by AI and may not be fully accurate, so please use your best judgement.", + }, + ], + ], + lastUpdated: true, + sitemap: { + hostname: "https://v3-docs.meteor.com", + }, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { + text: "Docs", + activeMatch: `^/(guide|docs|examples)/`, + items: [ + { text: "Quick Start", link: "/about/install" }, + { text: "Examples", link: "https://github.com/meteor/examples" }, + { + text: "Meteor.js 2 Docs", + link: "https://v2-docs.meteor.com", + }, + { + text: "Migration from Meteor.js 2", + link: "https://v3-migration-docs.meteor.com", + }, + { + text: "Tutorials", + items: [ + { + text: "Meteor.js 3 + React", + link: "/tutorials/react/index", + }, + { + text: "Meteor + Vue + vue-meteor-tracker", + link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", + }, + ], + }, + ], + }, + { + text: "Ecosystem", + activeMatch: `^/ecosystem/`, + items: [ + { + text: "Community & Help", + items: [ + { + text: "Meteor Forums", + link: "https://forums.meteor.com", + }, + { + text: "Meteor Lounge Discord", + link: "https://discord.gg/hZkTCaVjmT", + }, + { + text: "GitHub Discussions", + link: "https://github.com/meteor/meteor/discussions", + }, + ], + }, + { + text: "Resources", + items: [ + { + text: "Packages on Atmosphere", + link: "https://atmospherejs.com/", + }, + { + text: "VS Code Extension", + link: "https://marketplace.visualstudio.com/items?itemName=meteor-toolbox.meteor-toolbox", + }, + { + text: "DevTools - Chrome Extension", + link: "https://chromewebstore.google.com/detail/ibniinmoafhgbifjojidlagmggecmpgf", + }, + { + text: "DevTools - Firefox Extension", + link: "https://addons.mozilla.org/en-US/firefox/addon/meteor-devtools-evolved/", + }, + ], + }, + { + text: "Learning", + items: [ + { + text: "Meteor University", + link: "https://university.meteor.com", + }, + { + text: "Youtube Channel", + link: "https://www.youtube.com/@meteorsoftware", + }, + ], + }, + { + text: "News", + items: [ + { text: "Blog on Dev.to", link: "https://dev.to/meteor" }, + { text: "Blog on Medium", link: "https://blog.meteor.com" }, + { text: "Twitter", link: "https://x.com/meteorjs" }, + { + text: "LinkedIn", + link: "https://www.linkedin.com/company/meteor-software/", + }, + ], + }, + ], + }, + { text: "API", link: "/api/" }, + { text: "Galaxy Cloud", link: "https://www.meteor.com/cloud" }, + ], + sidebar: [ + { + text: "About", + link: "/about/what-is", + items: [ + { + text: "What is Meteor?", + link: "/about/what-is#introduction", + }, + { + text: "Meteor resources", + link: "/about/what-is#learning-more", + }, + { + text: "Roadmap", + link: "/about/roadmap", + }, + ], + collapsed: true, + }, + { + text: "Quick Start", + items: [ + { + text: "Install Meteor", + link: "/about/install", + }, + { + text: "Web Apps", + link: "/about/web-apps", + }, + { + text: "Cordova", + link: "/about/cordova", + }, + ], + collapsed: true, + }, + { + text: "API", + link: "/api/", + items: [ + { + text: "Accounts", + link: "/api/accounts", + items: [ + { text: "Accounts-Base", link: "/api/accounts#accounts-base" }, + { text: "Multi-server", link: "/api/accounts#multi-server" }, + { text: "Passwords", link: "/api/accounts#passwords" }, + ], + collapsed: true, + }, + { + text: "Meteor", + link: "/api/meteor", + items: [ + { text: "Core", link: "/api/meteor#core" }, + { text: "Methods", link: "/api/meteor#methods" }, + { text: "Publish and Subscribe", link: "/api/meteor#pubsub" }, + { text: "Server connections", link: "/api/meteor#connections" }, + { text: "Timers", link: "/api/meteor#timers" }, + ], + }, + { + text: "Collections", + link: "/api/collections", + }, + { + text: "DDPRateLimiter", + link: "/api/DDPRateLimiter", + }, + { + text: "Check", + link: "/api/check", + }, + { + text: "Session", + link: "/api/session", + }, + { + text: "Blaze", + link: "/api/blaze", + }, + { + text: "Templates", + link: "/api/templates", + }, + { + text: "Email", + link: "/api/email", + }, + { + text: "Tracker", + link: "/api/Tracker", + }, + { + text: "Reactive Var", + link: "/api/ReactiveVar", + }, + { + text: "Reactive Dict", + link: "/api/ReactiveDict", + }, + { + text: "EJSON", + link: "/api/EJSON", + }, + { + text: "Assets", + link: "/api/assets", + }, + { + text: "Mobile Configuration", + link: "/api/app", + }, + { + text: "Package.js", + link: "/api/package", + }, + { + text: "Top Level Await", + link: "/api/top-level-await", + }, + { + text: "Roles", + link: "/api/roles", + }, + ], + collapsed: true, + }, + { + text: "Packages", + items: [ + { + text: "accounts-ui", + link: "/packages/accounts-ui", + }, + { + text: "accounts-passwordless", + link: "/packages/accounts-passwordless", + }, + { + text: "accounts-2fa", + link: "/packages/accounts-2fa", + }, + { + text: "appcache", + link: "/packages/appcache", + }, + { + text: "audit-arguments-checks", + link: "/packages/audit-argument-checks", + }, + { + text: "autoupdate", + link: "/packages/autoupdate", + }, + { + text: "browser-policy", + link: "/packages/browser-policy", + }, + { + text: "bundler-visualizer", + link: "/packages/bundle-visualizer", + }, + { + text: "coffeescript", + link: "/packages/coffeescript", + }, + { + text: "ecmascript", + link: "/packages/ecmascript", + }, + { + text: "fetch", + link: "/packages/fetch", + }, + { + text: "hot-module-replacement", + link: "/packages/hot-module-replacement", + }, + { + text: "less", + link: "/packages/less", + }, + { + text: "logging", + link: "/packages/logging", + }, + { + text: "markdown", + link: "/packages/markdown", + }, + { + text: "modules", + link: "/packages/modules", + }, + { + text: "oauth-encryption", + link: "/packages/oauth-encryption", + }, + { + text: "random", + link: "/packages/random", + }, + { + text: "server-render", + link: "/packages/server-render", + }, + { + text: "standard-minifier-css", + link: "/packages/standard-minifier-css", + }, + { + text: "underscore", + link: "/packages/underscore", + }, + { + text: "url", + link: "/packages/url", + }, + { + text: "webapp", + link: "/packages/webapp", + }, + { + link: "/packages/packages-listing", + text: "Maintained Packages", + }, + { + link: "packages/community-packages", + text: "Community Packages", + }, + ], + collapsed: true, + }, + { + text: "Troubleshooting", + items: [ + { + text: "Expired Certificates", + link: "/troubleshooting/expired-certificate", + }, + { text: "Windows", link: "/troubleshooting/windows" }, + { + text: "Known issues in 2.13", + link: "/troubleshooting/known-issues", + }, + ], + collapsed: true, + }, + { + text: "Command Line", + items: [ + { link: "/cli/", text: "CLI" }, + { link: "/cli/using-core-types", text: "Using Core Types" }, + { link: "/cli/environment-variables", text: "Environment Variables" }, + ], + collapsed: true, + }, + { + text: "Tutorials", + items: [ + { + text: "Meteor.js 3 + React", + link: "/tutorials/react/index", + }, + { + link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", + text: "Meteor + Vue + vue-meteor-tracker", + }, + ], + collapsed: true, + }, + { + text: "Changelog", + items: [ + // TODO: Open issue in Vitepress about this + { link: "/history", text: "Meteor.js v3 (Current)" }, + { + link: "https://v2-docs.meteor.com/changelog", + text: "Meteor.js v2", + }, + { + link: "https://v2-docs.meteor.com/changelog#v112220211012", + text: "Meteor.js v1", + }, + ], + collapsed: true, + }, + ], + + socialLinks: [ + { icon: "github", link: "https://github.com/meteor/meteor" }, + { icon: "twitter", link: "https://x.com/meteorjs" }, + { icon: "discord", link: "https://discord.gg/hZkTCaVjmT" }, + ], + + logo: { dark: "/meteor-logo.png", light: "/meteor-blue.png" }, + + search: { + provider: "algolia", + options: { + appId: "2RBX3PR26I", + apiKey: "7fcba92008b84946f04369df2afa1744", + indexName: "meteor_docs_v3", + searchParameters: { + facetFilters: ["lang:en"], + }, + }, + }, + + footer: { + message: + 'Released under the MIT License.', + copyright: + 'Copyright (c) 2011 - present Meteor Software.', + }, + editLink: { + pattern: "https://github.com/meteor/meteor/edit/devel/v3-docs/docs/:path", + text: "Edit this page on GitHub", + }, + }, +}); diff --git a/v3-docs/docs/api/roles.md b/v3-docs/docs/api/roles.md new file mode 100644 index 0000000000..5ff06c9449 --- /dev/null +++ b/v3-docs/docs/api/roles.md @@ -0,0 +1,452 @@ +# Roles + +Authorization package for Meteor - compatible with built-in accounts package. + +## Installation + +To add roles to your application, run this command in your terminal: + +```bash +meteor add roles +``` + +## Overview + +The roles package lets you attach roles to users and then check against those roles when deciding whether to grant access to Meteor methods or publish data. The core concept is simple - you create role assignments for users and then verify those roles later. This package provides helper methods to make the process of adding, removing, and verifying roles easier. + +## Concepts + +### Roles vs Permissions + +Although named "roles", you can define your **roles**, **scopes** or **permissions** however you like. They are essentially tags assigned to users that you can check later. + +You can have traditional roles like `admin` or `webmaster`, or more granular permissions like `view-secrets`, `users.view`, or `users.manage`. Often, more granular permissions are better as they handle edge cases without creating many higher-level roles. + +### Role Hierarchy + +Roles can be organized in a hierarchy: + +- Roles can have multiple parents and children (subroles) +- If a parent role is assigned to a user, all its descendant roles also apply +- This allows creating "super roles" that aggregate permissions + +Example hierarchy setup: + +```js +import { Roles } from "meteor/roles"; + +// Create base roles +await Roles.createRoleAsync("user"); +await Roles.createRoleAsync("admin"); + +// Create permission roles +await Roles.createRoleAsync("USERS_VIEW"); +await Roles.createRoleAsync("POST_EDIT"); + +// Set up hierarchy +await Roles.addRolesToParentAsync("USERS_VIEW", "admin"); +await Roles.addRolesToParentAsync("POST_EDIT", "admin"); +await Roles.addRolesToParentAsync("POST_EDIT", "user"); +``` + +### Scopes + +Scopes allow users to have independent sets of roles. Use cases include: + +- Different communities within your app +- Multiple tenants in a multi-tenant application +- Different resource groups + +Users can have both scoped roles and global roles: + +- Global roles apply across all scopes +- Scoped roles only apply within their specific scope +- Scopes are independent of each other + +Example using scopes: + +```js +// Assign scoped roles +await Roles.addUsersToRolesAsync(userId, ["manage-team"], "team-a"); +await Roles.addUsersToRolesAsync(userId, ["player"], "team-b"); + +// Check scoped roles +await Roles.userIsInRoleAsync(userId, "manage-team", "team-a"); // true +await Roles.userIsInRoleAsync(userId, "manage-team", "team-b"); // false + +// Assign global role +await Roles.addUsersToRolesAsync(userId, "super-admin", null); + +// Global roles work in all scopes +await Roles.userIsInRoleAsync(userId, ["manage-team", "super-admin"], "team-b"); // true +``` + +## Role Management + + + +Example: + +```js +import { Roles } from "meteor/roles"; + +// Create a new role +await Roles.createRoleAsync("admin"); + +// Create if doesn't exist +await Roles.createRoleAsync("editor", { unlessExists: true }); +``` + +### Modifying Roles + + + +Example: + +```js +// Make 'editor' a child role of 'admin' +await Roles.addRolesToParentAsync("editor", "admin"); + +// Add multiple child roles +await Roles.addRolesToParentAsync(["editor", "moderator"], "admin"); +``` + + + +Example: + +```js +// Remove 'editor' as child role of 'admin' +await Roles.removeRolesFromParentAsync("editor", "admin"); +``` + + + +Example: + +```js +// Delete role and all its assignments +await Roles.deleteRoleAsync("temp-role"); +``` + + + +Example: + +```js +// Rename an existing role +await Roles.renameRoleAsync('editor', 'content-editor'); +``` + +### Assigning Roles + + + +Example: + +```js +// Add global roles +await Roles.addUsersToRolesAsync(userId, ["admin", "editor"]); + +// Add scoped roles +await Roles.addUsersToRolesAsync(userId, ["manager"], "department-a"); + +// Add roles to multiple users +await Roles.addUsersToRolesAsync([user1Id, user2Id], ["user"]); +``` + + + +Example: + +```js +// Replace user's global roles +await Roles.setUserRolesAsync(userId, ["editor"]); + +// Replace scoped roles +await Roles.setUserRolesAsync(userId, ["viewer"], "project-x"); + +// Clear all roles in scope +await Roles.setUserRolesAsync(userId, [], "project-x"); +``` + + + +Example: + +```js +// Remove global roles +await Roles.removeUsersFromRolesAsync(userId, ["admin"]); + +// Remove scoped roles +await Roles.removeUsersFromRolesAsync(userId, ["manager"], "department-a"); + +// Remove roles from multiple users +await Roles.removeUsersFromRolesAsync([user1Id, user2Id], ["temp-role"]); +``` + + + +Example: + +```js +// Rename a scope +await Roles.renameScopeAsync('department-1', 'marketing'); +``` + + + +Example: + +```js +// Remove a scope and all its role assignments +await Roles.removeScopeAsync('old-department'); +``` + + + +Example: + +```js +// Get all roles sorted by name +const roles = Roles.getAllRoles({ sort: { _id: 1 } }); + +// Get roles with custom query +const customRoles = Roles.getAllRoles({ + fields: { _id: 1, children: 1 }, + sort: { _id: -1 } +}); +``` + + + +Example: + +```js +// Find all admin users +const adminUsers = await Roles.getUsersInRoleAsync('admin'); + +// Find users with specific roles in a scope +const scopedUsers = await Roles.getUsersInRoleAsync(['editor', 'writer'], 'blog'); + +// Find users with custom options +const users = await Roles.getUsersInRoleAsync('manager', { + scope: 'department-a', + queryOptions: { + sort: { createdAt: -1 }, + limit: 10 + } +}); +``` + +## Checking Roles + + + +Example: + +```js +// Check global role +const isAdmin = await Roles.userIsInRoleAsync(userId, "admin"); + +// Check any of multiple roles +const canEdit = await Roles.userIsInRoleAsync(userId, ["editor", "admin"]); + +// Check scoped role +const isManager = await Roles.userIsInRoleAsync( + userId, + "manager", + "department-a" +); + +// Check role in any scope +const hasRole = await Roles.userIsInRoleAsync(userId, "viewer", { + anyScope: true, +}); +``` + + + +Example: + +```js +// Get user's global roles +const globalRoles = await Roles.getRolesForUserAsync(userId); + +// Get scoped roles +const deptRoles = await Roles.getRolesForUserAsync(userId, "department-a"); + +// Get all roles including inherited +const allRoles = await Roles.getRolesForUserAsync(userId, { + anyScope: true, + fullObjects: true, +}); +``` + + + +Example: + +```js +// Check if admin is a parent of editor +const isParent = await Roles.isParentOfAsync('admin', 'editor'); + +// Can be used to check inheritance chains +const hasPermission = await Roles.isParentOfAsync('super-admin', 'post-edit'); +``` + + + +Example: + +```js +// Get all scopes for user +const allScopes = await Roles.getScopesForUserAsync(userId); + +// Get scopes where user has specific roles +const editorScopes = await Roles.getScopesForUserAsync(userId, ['editor']); +``` + +## Publishing Roles + +Role assignments need to be published to be available on the client. Example publication: + +```js +// Publish user's own roles +Meteor.publish(null, function () { + if (this.userId) { + return Meteor.roleAssignment.find({ "user._id": this.userId }); + } + this.ready(); +}); + +// Publish roles for specific scope +Meteor.publish("scopeRoles", function (scope) { + if (this.userId) { + return Meteor.roleAssignment.find({ scope: scope }); + } + this.ready(); +}); +``` + +## Using with Templates + +The roles package automatically provides an `isInRole` helper for templates: + +```handlebars +{{#if isInRole "admin"}} +
+ +
+{{/if}} + +{{#if isInRole "editor,writer" "blog"}} +
+ +
+{{/if}} +``` + +## Migration to Core Version + +If you are currently using the `alanning:roles` package, follow these steps to migrate to the core version: + +1. Make sure you are on version 3.6 of `alanning:roles` first +2. Run any pending migrations from previous versions +3. Switch all server-side role operations to use the async versions of the functions: + - createRoleAsync + - deleteRoleAsync + - addUsersToRolesAsync + - setUserRolesAsync + - removeUsersFromRolesAsync + - etc. +4. Remove `alanning:roles` package: + ```bash + meteor remove alanning:roles + ``` +5. Add the core roles package: + ```bash + meteor add roles + ``` + +The sync versions of these functions are still available on the client. + +## Security Considerations + +1. Client-side role checks are for convenience only - always verify permissions on the server +2. Publish only the role data that users need +3. Use scopes to properly isolate role assignments +4. Validate role names and scopes to prevent injection attacks +5. Consider using more granular permissions over broad role assignments + +## Example Usage + +### Method Security + +```js +// server/methods.js +Meteor.methods({ + deletePost: async function (postId) { + check(postId, String); + + const canDelete = await Roles.userIsInRoleAsync( + this.userId, + ["admin", "moderator"], + "posts" + ); + + if (!canDelete) { + throw new Meteor.Error("unauthorized", "Not authorized to delete posts"); + } + + Posts.remove(postId); + }, +}); +``` + +### Publication Security + +```js +// server/publications.js +Meteor.publish("secretDocuments", async function (scope) { + check(scope, String); + + const canView = await Roles.userIsInRoleAsync( + this.userId, + ["view-secrets", "admin"], + scope + ); + + if (canView) { + return SecretDocs.find({ scope: scope }); + } + + this.ready(); +}); +``` + +### User Management + +```js +// server/users.js +Meteor.methods({ + promoteToEditor: async function (userId, scope) { + check(userId, String); + check(scope, String); + + const canPromote = await Roles.userIsInRoleAsync( + this.userId, + "admin", + scope + ); + + if (!canPromote) { + throw new Meteor.Error("unauthorized"); + } + + await Roles.addUsersToRolesAsync(userId, ["editor"], scope); + }, +}); +``` + + From 23a0c254fb51477c3817a685e53b78f6cc5c9f0c Mon Sep 17 00:00:00 2001 From: denihs Date: Thu, 14 Nov 2024 10:50:29 -0400 Subject: [PATCH 02/32] force CI tests to run again From eccbcd22785742100f4b0194b7b120cd302f7a14 Mon Sep 17 00:00:00 2001 From: denihs Date: Thu, 14 Nov 2024 10:52:22 -0400 Subject: [PATCH 03/32] force CI tests to run again From 53a2f665b0586750ce331d3722e62552d28f7190 Mon Sep 17 00:00:00 2001 From: denihs Date: Thu, 14 Nov 2024 11:44:02 -0400 Subject: [PATCH 04/32] - adding since when roles package is available --- v3-docs/docs/api/roles.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v3-docs/docs/api/roles.md b/v3-docs/docs/api/roles.md index 5ff06c9449..40c6e69e49 100644 --- a/v3-docs/docs/api/roles.md +++ b/v3-docs/docs/api/roles.md @@ -2,6 +2,8 @@ Authorization package for Meteor - compatible with built-in accounts package. +> Available since Meteor 3.1.0 + ## Installation To add roles to your application, run this command in your terminal: From 77d4496350511d4a1d5c90a1250a879bf3c5479d Mon Sep 17 00:00:00 2001 From: denihs Date: Thu, 14 Nov 2024 12:00:28 -0400 Subject: [PATCH 05/32] - adding since when roles package is available --- v3-docs/docs/api/roles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/api/roles.md b/v3-docs/docs/api/roles.md index 40c6e69e49..fdaee6c473 100644 --- a/v3-docs/docs/api/roles.md +++ b/v3-docs/docs/api/roles.md @@ -2,7 +2,7 @@ Authorization package for Meteor - compatible with built-in accounts package. -> Available since Meteor 3.1.0 +> Available since Meteor 3.1.0 (previously alanning:roles) ## Installation From 804fcbb483640dbfe3bac4b25f872c3a6031394d Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Thu, 28 Nov 2024 09:20:31 -0300 Subject: [PATCH 06/32] DOCS: Add redirect for /changelog --- .../.vitepress/theme/redirects/redirects.json | 3 ++- .../docs/.vitepress/theme/redirects/script.js | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/v3-docs/docs/.vitepress/theme/redirects/redirects.json b/v3-docs/docs/.vitepress/theme/redirects/redirects.json index f6dc86f381..d57349da0c 100644 --- a/v3-docs/docs/.vitepress/theme/redirects/redirects.json +++ b/v3-docs/docs/.vitepress/theme/redirects/redirects.json @@ -1,4 +1,5 @@ { "meteor_call": "meteor-call", - "ddp_connect": "DDP-connect" + "ddp_connect": "DDP-connect", + "changelog": "history" } diff --git a/v3-docs/docs/.vitepress/theme/redirects/script.js b/v3-docs/docs/.vitepress/theme/redirects/script.js index 0e7268824c..6721d68b7d 100644 --- a/v3-docs/docs/.vitepress/theme/redirects/script.js +++ b/v3-docs/docs/.vitepress/theme/redirects/script.js @@ -1,18 +1,26 @@ -// import json from './redirects.json'; +import redirects from "./redirects.json"; /** * * @param {string} path */ export const redirect = (path) => { - let shouldRedirect = false; - console.log(path) + const lastPath = path.split("/").pop().split(".")[0]; + if (redirects[lastPath]) { + return { + path: path.replace(lastPath, redirects[lastPath]), + shouldRedirect: true, + }; + } + if (path.includes("_")) { - shouldRedirect = true; - path = path.replace("_", "-"); + return { + path: path.replace("_", "-"), + shouldRedirect: true, + }; } return { path, - shouldRedirect + shouldRedirect: false, }; }; From 7ebd1137261a29aa2f6af4ab8f34f1708cd842bd Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Thu, 28 Nov 2024 21:09:30 -0300 Subject: [PATCH 07/32] DOCS: Add community packages page and react-meteor-data package --- v3-docs/docs/.vitepress/config.mts | 15 + v3-docs/docs/api/packages-listing.md | 1 - v3-docs/docs/community-packages/index.md | 10 + v3-docs/docs/packages/react-meteor-data.md | 491 +++++++++++++++++++++ 4 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 v3-docs/docs/community-packages/index.md create mode 100644 v3-docs/docs/packages/react-meteor-data.md diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index a004c2867b..90e1963f61 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -346,6 +346,10 @@ export default defineConfig({ text: "random", link: "/packages/random", }, + { + text: "react-meteor-data", + link: "/packages/react-meteor-data", + }, { text: "server-render", link: "/packages/server-render", @@ -377,6 +381,17 @@ export default defineConfig({ ], collapsed: true, }, + { + text: "Community Packages", + link: "/community-packages/index", + items: [ + { + text: "React Meteor Data", + link: "/community-packages/react-meteor-data", + }, + ], + collapsed: true, + }, { text: "Troubleshooting", items: [ diff --git a/v3-docs/docs/api/packages-listing.md b/v3-docs/docs/api/packages-listing.md index 40d21881ff..c4624c061e 100644 --- a/v3-docs/docs/api/packages-listing.md +++ b/v3-docs/docs/api/packages-listing.md @@ -45,7 +45,6 @@ ### [callback-hook](https://github.com/meteor/meteor/tree/devel/packages/callback-hook) {#callback-hook} ### [check](https://github.com/meteor/meteor/tree/devel/packages/check) {#check} ### [constraint-solver](https://github.com/meteor/meteor/tree/devel/packages/constraint-solver) {#constraint-solver} -### [context](https://github.com/meteor/meteor/tree/devel/packages/context) {#context} ### [core-runtime](https://github.com/meteor/meteor/tree/devel/packages/core-runtime) {#core-runtime} ### [crosswalk](https://github.com/meteor/meteor/tree/devel/packages/crosswalk) {#crosswalk} ### [ddp](https://github.com/meteor/meteor/tree/devel/packages/ddp) {#ddp} diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md new file mode 100644 index 0000000000..8cc5317613 --- /dev/null +++ b/v3-docs/docs/community-packages/index.md @@ -0,0 +1,10 @@ +# Community Packages + +There are some very popular community packages that do not have a documentation website or only a readme file. +This section tries to list and add some information about usage, configuration, and examples for these packages. + +This section is a work in progress and it is community-driven. +If you use or have a package that you think would be useful to add to this list, please open a pull request. + +## List of Community Packages + diff --git a/v3-docs/docs/packages/react-meteor-data.md b/v3-docs/docs/packages/react-meteor-data.md new file mode 100644 index 0000000000..50717c82b1 --- /dev/null +++ b/v3-docs/docs/packages/react-meteor-data.md @@ -0,0 +1,491 @@ +# react-meteor-data + +This package provides an integration between React and [`Tracker`](https://atmospherejs.com/meteor/tracker), Meteor's reactive data system. + +## Table of Contents + +[[toc]] + +## Install + +::: tip + +This package is included with `meteor create` on react options. No need to install it manually. + +::: + +To install the package, use `meteor add`: + +```bash +meteor add react-meteor-data +``` + +You'll also need to install `react` if you have not already: + +```bash +meteor npm install react +``` + +### Changelog + +[check recent changes here](https://github.com/meteor/react-packages/blob/master/packages/react-meteor-data/CHANGELOG.md) + +## Usage + +This package provides two ways to use Tracker reactive data in your React components: + +- a hook: `useTracker` (v2 only, requires React `^16.8`) +- a higher-order component (HOC): `withTracker` (v1 and v2). + +The `useTracker` hook, introduced in version 2.0.0, embraces the [benefits of hooks](https://reactjs.org/docs/hooks-faq.html). Like all React hooks, it can only be used in function components, not in class components. + +The `withTracker` HOC can be used with all components, function or class based. + +It is not necessary to rewrite existing applications to use the `useTracker` hook instead of the existing `withTracker` HOC. + +### `useTracker(reactiveFn)` + +You can use the `useTracker` hook to get the value of a Tracker reactive function in your React "function components." The reactive function will get re-run whenever its reactive inputs change, and the component will re-render with the new value. + +`useTracker` manages its own state, and causes re-renders when necessary. There is no need to call React state setters from inside your `reactiveFn`. Instead, return the values from your `reactiveFn` and assign those to variables directly. When the `reactiveFn` updates, the variables will be updated, and the React component will re-render. + +Arguments: + +- `reactiveFn`: A Tracker reactive function (receives the current computation). + +The basic way to use `useTracker` is to simply pass it a reactive function, with no further fuss. This is the preferred configuration in many cases. + +#### `useTracker(reactiveFn, deps)` + +You can pass an optional deps array as a second value. When provided, the computation will be retained, and reactive updates after the first run will run asynchronously from the react render execution frame. This array typically includes all variables from the outer scope "captured" in the closure passed as the 1st argument. For example, the value of a prop used in a subscription or a minimongo query; see example below. + +This should be considered a low level optimization step for cases where your computations are somewhat long running - like a complex minimongo query. In many cases it's safe and even preferred to omit deps and allow the computation to run synchronously with render. + +Arguments: + +- `reactiveFn` +- `deps`: An optional array of "dependencies" of the reactive function. This is very similar to how the `deps` argument for [React's built-in `useEffect`, `useCallback` or `useMemo` hooks](https://reactjs.org/docs/hooks-reference.html) work. + +```jsx +import { useTracker } from 'meteor/react-meteor-data'; + +// React function component. +function Foo({ listId }) { + // This computation uses no value from the outer scope, + // and thus does not needs to pass a 'deps' argument. + // However, we can optimize the use of the computation + // by providing an empty deps array. With it, the + // computation will be retained instead of torn down and + // rebuilt on every render. useTracker will produce the + // same results either way. + const currentUser = useTracker(() => Meteor.user(), []); + + // The following two computations both depend on the + // listId prop. When deps are specified, the computation + // will be retained. + const listLoading = useTracker(() => { + // Note that this subscription will get cleaned up + // when your component is unmounted or deps change. + const handle = Meteor.subscribe('todoList', listId); + return !handle.ready(); + }, [listId]); + const tasks = useTracker(() => Tasks.find({ listId }).fetch(), [listId]); + + return ( +

Hello {currentUser.username}

+ {listLoading ? ( +
Loading
+ ) : ( +
+ Here is the Todo list {listId}: +
    + {tasks.map(task => ( +
  • {task.label}
  • + ))} +
+
+ )} + ); +} +``` + +**Note:** the [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) package provides ESLint hints to help detect missing values in the `deps` argument of React built-in hooks. It can be configured to also validate the `deps` argument of the `useTracker` hook or some other hooks, with the following `eslintrc` config: + +```json +"react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useTracker|useSomeOtherHook|..." }] +``` + +#### `useTracker(reactiveFn, deps, skipUpdate)` or `useTracker(reactiveFn, skipUpdate)` + +You may optionally pass a function as a second or third argument. The `skipUpdate` function can evaluate the return value of `reactiveFn` for changes, and control re-renders in sensitive cases. _Note:_ This is not meant to be used with a deep compare (even fast-deep-equals), as in many cases that may actually lead to worse performance than allowing React to do it's thing. But as an example, you could use this to compare an `updatedAt` field between updates, or a subset of specific fields, if you aren't using the entire document in a subscription. As always with any optimization, measure first, then optimize second. Make sure you really need this before implementing it. + +Arguments: + +- `reactiveFn` +- `deps?` - optional - you may omit this, or pass a "falsy" value. +- `skipUpdate` - A function which receives two arguments: `(prev, next) => (prev === next)`. `prev` and `next` will match the type or data shape as that returned by `reactiveFn`. Note: A return value of `true` means the update will be "skipped". `false` means re-render will occur as normal. So the function should be looking for equivalence. + +```jsx +import { useTracker } from 'meteor/react-meteor-data'; + +// React function component. +function Foo({ listId }) { + const tasks = useTracker( + () => Tasks.find({ listId }).fetch(), [listId], + (prev, next) => { + // prev and next will match the type returned by the reactiveFn + return prev.every((doc, i) => ( + doc._id === next[i] && doc.updatedAt === next[i] + )) && prev.length === next.length; + } + ); + + return ( +

Hello {currentUser.username}

+
+ Here is the Todo list {listId}: +
    + {tasks.map(task => ( +
  • {task.label}
  • + ))} +
+
+ ); +} +``` + +### `withTracker(reactiveFn)` + +You can use the `withTracker` HOC to wrap your components and pass them additional props values from a Tracker reactive function. The reactive function will get re-run whenever its reactive inputs change, and the wrapped component will re-render with the new values for the additional props. + +Arguments: + +- `reactiveFn`: a Tracker reactive function, getting the props as a parameter, and returning an object of additional props to pass to the wrapped component. + +```jsx +import { withTracker } from 'meteor/react-meteor-data'; + +// React component (function or class). +function Foo({ listId, currentUser, listLoading, tasks }) { + return ( +

Hello {currentUser.username}

+ {listLoading ? +
Loading
: +
+ Here is the Todo list {listId}: +
    {tasks.map(task =>
  • {task.label}
  • )}
+ { + // Do all your reactive data access in this function. + // Note that this subscription will get cleaned up when your component is unmounted + const handle = Meteor.subscribe('todoList', listId); + + return { + currentUser: Meteor.user(), + listLoading: !handle.ready(), + tasks: Tasks.find({ listId }).fetch(), + }; +})(Foo); +``` + +The returned component will, when rendered, render `Foo` (the "lower-order" component) with its provided props in addition to the result of the reactive function. So `Foo` will receive `{ listId }` (provided by its parent) as well as `{ currentUser, listLoading, tasks }` (added by the `withTracker` HOC). + +For more information, see the [React article](http://guide.meteor.com/react.html) in the Meteor Guide. + +### `withTracker({ reactiveFn, pure, skipUpdate })` + +The `withTracker` HOC can receive a config object instead of a simple reactive function. + +- `getMeteorData` - The `reactiveFn`. +- `pure` - `true` by default. Causes the resulting Container to be wrapped with React's `memo()`. +- `skipUpdate` - A function which receives two arguments: `(prev, next) => (prev === next)`. `prev` and `next` will match the type or data shape as that returned by `reactiveFn`. Note: A return value of `true` means the update will be "skipped". `false` means re-render will occur as normal. So the function should be looking for equivalence. + +```jsx +import { withTracker } from 'meteor/react-meteor-data'; + +// React component (function or class). +function Foo({ listId, currentUser, listLoading, tasks }) { + return ( +

Hello {currentUser.username}

+ {listLoading ? +
Loading
: +
+ Here is the Todo list {listId}: +
    {tasks.map(task =>
  • {task.label}
  • )}
+ ( + doc._id === next[i] && doc.updatedAt === next[i] + )) + && prev.tasks.length === next.tasks.length + ); + } +})(Foo); +``` + +### `useSubscribe(subName, ...args)` + +`useSubscribe` is a convenient short hand for setting up a subscription. It is particularly useful when working with `useFind`, which should NOT be used for setting up subscriptions. At its core, it is a very simple wrapper around `useTracker` (with no deps) to create the subscription in a safe way, and allows you to avoid some of the ceremony around defining a factory and defining deps. Just pass the name of your subscription, and your arguments. + +`useSubscribe` returns an `isLoading` function. You can call `isLoading()` to react to changes in the subscription's loading state. The `isLoading` function will both return the loading state of the subscription, and set up a reactivity for the loading state change. If you don't call this function, no re-render will occur when the loading state changes. + +```jsx +// Note: isLoading is a function! +const isLoading = useSubscribe("posts", groupId); +const posts = useFind(() => Posts.find({ groupId }), [groupId]); + +if (isLoading()) { + return ; +} else { + return ( +
    + {posts.map((post) => ( +
  • {post.title}
  • + ))} +
+ ); +} +``` + +If you want to conditionally subscribe, you can set the `name` field (the first argument) to a falsy value to bypass the subscription. + +```jsx +const needsData = false; +const isLoading = useSubscribe(needsData ? "my-pub" : null); + +// When a subscription is not used, isLoading() will always return false +``` + +### `useFind(cursorFactory, deps)` + +The `useFind` hook can substantially speed up the rendering (and rerendering) of lists coming from mongo queries (subscriptions). It does this by controlling document object references. By providing a highly tailored cursor management within the hook, using the `Cursor.observe` API, `useFind` carefully updates only the object references changed during a DDP update. This approach allows a tighter use of core React tools and philosophies to turbo charge your list renders. It is a very different approach from the more general purpose `useTracker`, and it requires a bit more set up. A notable difference is that you should NOT call `.fetch()`. `useFind` requires its factory to return a `Mongo.Cursor` object. You may also return `null`, if you want to conditionally set up the Cursor. + +Here is an example in code: + +```jsx +import React, { memo } from "react"; +import { useFind } from "meteor/react-meteor-data"; +import TestDocs from "/imports/api/collections/TestDocs"; + +// Memoize the list item +const ListItem = memo(({ doc }) => { + return ( +
  • + {doc.id},{doc.updated} +
  • + ); +}); + +const Test = () => { + const docs = useFind(() => TestDocs.find(), []); + return ( +
      + {docs.map((doc) => ( + + ))} +
    + ); +}; + +// Later on, update a single document - notice only that single component is updated in the DOM +TestDocs.update({ id: 2 }, { $inc: { someProp: 1 } }); +``` + +If you want to conditionally call the find method based on some props configuration or anything else, return `null` from the factory. + +```jsx +const docs = useFind(() => { + if (props.skip) { + return null; + } + return TestDocs.find(); +}, []); +``` + +### Concurrent Mode, Suspense and Error Boundaries + +There are some additional considerations to keep in mind when using Concurrent Mode, Suspense and Error Boundaries, as +each of these can cause React to cancel and discard (toss) a render, including the result of the first run of your +reactive function. One of the things React developers often stress is that we should not create "side-effects" directly +in the render method or in functional components. There are a number of good reasons for this, including allowing the +React runtime to cancel renders. Limiting the use of side-effects allows features such as concurrent mode, suspense and +error boundaries to work deterministically, without leaking memory or creating rogue processes. Care should be taken to +avoid side effects in your reactive function for these reasons. (Note: this caution does not apply to Meteor specific +side-effects like subscriptions, since those will be automatically cleaned up when `useTracker`'s computation is +disposed.) + +Ideally, side-effects such as creating a Meteor computation would be done in `useEffect`. However, this is problematic +for Meteor, which mixes an initial data query with setting up the computation to watch those data sources all in one +initial run. If we wait to do that in `useEffect`, we'll end up rendering a minimum of 2 times (and using hacks for the +first one) for every component which uses `useTracker` or `withTracker`, or not running at all in the initial render and +still requiring a minimum of 2 renders, and complicating the API. + +To work around this and keep things running fast, we are creating the computation in the render method directly, and +doing a number of checks later in `useEffect` to make sure we keep that computation fresh and everything up to date, +while also making sure to clean things up if we detect the render has been tossed. For the most part, this should all be +transparent. + +The important thing to understand is that your reactive function can be initially called more than once for a single +render, because sometimes the work will be tossed. Additionally, `useTracker` will not call your reactive function +reactively until the render is committed (until `useEffect` runs). If you have a particularly fast changing data source, +this is worth understanding. With this very short possible suspension, there are checks in place to make sure the +eventual result is always up to date with the current state of the reactive function. Once the render is "committed", +and the component mounted, the computation is kept running, and everything will run as expected. + +## Suspendable version of hooks + +### `useTracker` + +This is a version of `useTracker` that can be used with React Suspense. + +For its first argument, a key is necessary, witch is used to identify the computation and to avoid recreating it when the +component is re-rendered. + +Its second argument is a function that can be async and reactive, +this argument works similar to the original `useTracker` that does not suspend. + +For its _optional_ third argument, the dependency array, works similar to the `useTracker` that does not suspend, +you pass in an array of variables that this tracking function depends upon. + +For its _optional_ fourth argument, the options object, works similar to the `useTracker` that does not suspend, +you pass in a function for when should skip the update. + +```jsx +import { useTracker } from "meteor/react-meteor-data/suspense"; +import { useSubscribe } from "meteor/react-meteor-data/suspense"; + +function Tasks() { + // this component will suspend + useSubscribe("tasks"); + const { username } = useTracker("user", () => Meteor.user()); // Meteor.user() is async meteor 3.0 + const tasksByUser = useTracker( + "tasksByUser", + () => + TasksCollection.find( + { username }, + { sort: { createdAt: -1 } } + ).fetchAsync() // async call + ); + + // render the tasks +} +``` + +### Maintaining the reactive context + +To maintain a reactive context using the new Meteor Async methods, we are using the new `Tracker.withComputation` API to maintain the reactive context of an +async call, this is needed because otherwise it would be only called once, and the computation would never run again, +this way, every time we have a new Link being added, this useTracker is ran. + +```jsx +// needs Tracker.withComputation because otherwise it would be only called once, and the computation would never run again +const docs = useTracker("name", async (c) => { + const placeholders = await fetch( + "https://jsonplaceholder.typicode.com/todos" + ).then((x) => x.json()); + console.log(placeholders); + return await Tracker.withComputation(c, () => + LinksCollection.find().fetchAsync() + ); +}); +``` + +A rule of thumb is that if you are using a reactive function for example `find` + `fetchAsync`, it is nice to wrap it +inside `Tracker.withComputation` to make sure that the computation is kept alive, if you are just calling that function +that is not necessary, like the one bellow, will be always reactive. + +```jsx +const docs = useTracker("name", () => LinksCollection.find().fetchAsync()); +``` + +### `useSubscribe` + +This is a version of `useSubscribe` that can be used with React Suspense. +It is similar to `useSubscribe`, it throws a promise and suspends the rendering until the promise is resolved. +It does not return a Meteor Handle to control the subscription + +```jsx +import { useTracker } from "meteor/react-meteor-data/suspense"; +import { useSubscribe } from "meteor/react-meteor-data/suspense"; + +function Tasks() { + // this component will suspend + useSubscribe("tasks"); + const { username } = useTracker("user", () => Meteor.user()); // Meteor.user() is async meteor 3.0 + const tasksByUser = useTracker( + "tasksByUser", + () => + TasksCollection.find( + { username }, + { sort: { createdAt: -1 } } + ).fetchAsync() // async call + ); + + // render the tasks +} +``` + +### `useFind` + +This is a version of `useFind` that can be used with React Suspense. +It has a few differences from the `useFind` without suspense, it throws a promise and suspends the rendering until the promise is resolved. +It returns the result and it is reactive. +You should pass as the first parameter the collection where is being searched upon and as the second parameter an array with the arguments, +the same arguments that you would pass to the `find` method of the collection, third parameter is optional, and it is dependency array object. +It's meant for the SSR, you don't have to use it if you're not interested in SSR. + +```jsx +import { useFind } from "meteor/react-meteor-data/suspense"; +import { useSubscribe } from "meteor/react-meteor-data/suspense"; + +function Tasks() { + // this component will suspend + useSubscribe("tasks"); + const tasksByUser = useFind(TasksCollection, [ + {}, + { sort: { createdAt: -1 } }, + ]); + + // render the tasks +} +``` + +## Version compatibility notes + +- `react-meteor-data` v2.x : + + - `useTracker` hook + `withTracker` HOC + - Requires React `^16.8`. + - Implementation is compatible with "React Suspense", concurrent mode and error boundaries. + - The `withTracker` HOC is strictly backwards-compatible with the one provided in v1.x, the major version number is only motivated by the bump of React version requirement. Provided a compatible React version, existing Meteor apps leveraging the `withTracker` HOC can freely upgrade from v1.x to v2.x, and gain compatibility with future React versions. + - The previously deprecated `createContainer` has been removed. + +- `react-meteor-data` v0.x : + - `withTracker` HOC (+ `createContainer`, kept for backwards compatibility with early v0.x releases) + - Requires React `^15.3` or `^16.0`. + - Implementation relies on React lifecycle methods (`componentWillMount` / `componentWillUpdate`) that are [marked for deprecation in future React versions](https://reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes) ("React Suspense"). From 419f8ec5bd39319707eb4b781ecf2ab2a62a052f Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Thu, 28 Nov 2024 21:11:01 -0300 Subject: [PATCH 08/32] DOCS: Update the community packages links --- v3-docs/docs/.vitepress/config.mts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 90e1963f61..5cf32b8890 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -384,12 +384,7 @@ export default defineConfig({ { text: "Community Packages", link: "/community-packages/index", - items: [ - { - text: "React Meteor Data", - link: "/community-packages/react-meteor-data", - }, - ], + items: [], collapsed: true, }, { From b6157fe37b6a0787c3c0f87697891e34ef48fba5 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba <70247653+Grubba27@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:47:25 -0300 Subject: [PATCH 09/32] Update v3-docs/docs/community-packages/index.md Co-authored-by: Leonardo Venturini --- v3-docs/docs/community-packages/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index 8cc5317613..fbe9df55db 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -1,6 +1,6 @@ # Community Packages -There are some very popular community packages that do not have a documentation website or only a readme file. +There are some very popular community packages that do not have a documentation website or only have a readme file. This section tries to list and add some information about usage, configuration, and examples for these packages. This section is a work in progress and it is community-driven. From 4c30c531350412e4026db8d72420de752faf03ea Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 2 Dec 2024 11:11:13 -0300 Subject: [PATCH 10/32] DOCS: refactor and update community packages page --- v3-docs/docs/community-packages/index.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index fbe9df55db..3c23fad224 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -3,8 +3,14 @@ There are some very popular community packages that do not have a documentation website or only have a readme file. This section tries to list and add some information about usage, configuration, and examples for these packages. -This section is a work in progress and it is community-driven. +> Think this section as an `Awesome List`, similar to [`awesome-node`](https://github.com/sindresorhus/awesome-nodejs) or [`awesome-react`](https://github.com/enaqx/awesome-react) + If you use or have a package that you think would be useful to add to this list, please open a pull request. -## List of Community Packages +Please bear in mind if you are adding a package to this list, try providing as much information as possible, including: +- `Who maintains the package` – how to get in touch to submit issues or questions +- `Why is this package for?` +- `API` +- `examples/guide` +## List of Community Packages From 2ceab9fc2ef8f07619e376055739e3f5dfa18892 Mon Sep 17 00:00:00 2001 From: Leonardo Venturini Date: Mon, 2 Dec 2024 13:31:07 -0400 Subject: [PATCH 11/32] point out that rosetta is no longer needed in 3.1 --- v3-docs/docs/about/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/about/install.md b/v3-docs/docs/about/install.md index dd8331d501..1d685e9ec3 100644 --- a/v3-docs/docs/about/install.md +++ b/v3-docs/docs/about/install.md @@ -13,7 +13,7 @@ npx meteor - Meteor currently supports **OS X, Windows, and Linux**. Only 64-bit is supported. - Meteor supports Windows 7 / Windows Server 2008 R2 and up. - Apple M1 is natively supported from Meteor 2.5.1 onward (for older versions, rosetta terminal is required). -- If you are on a Mac M1 (Arm64 version) you need to have Rosetta 2 installed, as Meteor uses it for running MongoDB. Check how to install it [here](https://osxdaily.com/2020/12/04/how-install-rosetta-2-apple-silicon-mac/). +- If you are using Meteor <= 3.0.4 and you are on a Mac M1 (Arm64 version) you need to have Rosetta 2 installed, as Meteor uses it for running MongoDB. Check how to install it [here](https://osxdaily.com/2020/12/04/how-install-rosetta-2-apple-silicon-mac/). *No longer needed in Meteor 3.1*. - Disabling antivirus (Windows Defender, etc.) will improve performance. - For compatibility, Linux binaries are built with CentOS 6.4 i386/amd64. From cd27b01f10dd92c1aa4a7416f617ec1b355e05fb Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 2 Dec 2024 20:19:32 -0300 Subject: [PATCH 12/32] DOCS: Add meteor-rpc to community packages --- v3-docs/docs/community-packages/index.md | 4 + v3-docs/docs/community-packages/meteor-rpc.md | 569 ++++++++++++++++++ 2 files changed, 573 insertions(+) create mode 100644 v3-docs/docs/community-packages/meteor-rpc.md diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index 3c23fad224..8c73cae2b0 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -14,3 +14,7 @@ Please bear in mind if you are adding a package to this list, try providing as m - `examples/guide` ## List of Community Packages + +#### Method/Subscription helpers + +- [`meteor-rpc`](./meteor-rpc.md) \ No newline at end of file diff --git a/v3-docs/docs/community-packages/meteor-rpc.md b/v3-docs/docs/community-packages/meteor-rpc.md new file mode 100644 index 0000000000..007746dc1f --- /dev/null +++ b/v3-docs/docs/community-packages/meteor-rpc.md @@ -0,0 +1,569 @@ +# Meteor-RPC + +- `Who maintains the package` – [Grubba27](https://github.com/Grubba27), you can get in touch via [X](https://twitter.com/gab_grubba) + +[[toc]] + +## What is this package? + +_Inspired on [zodern:relay](https://github.com/zodern/meteor-relay) and on [tRPC](https://trpc.io/)_ + +This package provides functions for building E2E type-safe RPCs, focused on React front ends. + +## How to download it? + +```bash +meteor npm i grubba-rpc @tanstack/react-query zod +``` + +::: warning + +Before continuing the installation make sure you have `react-query` all set in your project, for more info follow their [quick start guide](https://tanstack.com/query/latest/docs/framework/react/quick-start). + +::: + +## How to use it? + +There is a few concepts that are important while using this package: + + + +- This package is built on top of [`Meteor.methods`](../api/meteor.md#method-apis-methods) and [`Meteor.publish`](../api/meteor.md#publish-and-subscribe-pubsub) but with types and runtime validation, their understanding is important to use this package. +- Every method and publication use `zod` to validate the arguments, so you can be sure that the data you are receiving is the data you are expecting. + +::: tip +If you are accepting any type of data you can use `z.any()` as the schema or `z.void` for when there is no argument +::: + +### `createModule` + +This function is used to create a module that will be used to call our methods and publications + +`subModule` without a namespace: `createModule()` is used to create the `main` server module, the one that will be exported to be used in the client.` + +`subModule` with a namespace: `createModule("namespace")` is used to create a submodule that will be added to the main module. + +> Remember to use `build` at the end of module creation to ensure that the module is going to be created. + +Example: + +::: code-group + +```typescript [server/main.ts] +import { createModule } from "grubba-rpc"; +import { Chat } from "./chat"; + +const server = createModule() // server has no namespace + .addMethod("bar", z.string(), (arg) => "bar" as const) + .addSubmodule(Chat) + .build(); + +export type Server = typeof server; +``` + +```typescript [server/chat.ts] +import { createModule } from "grubba-rpc"; +import { ChatCollection } from "/imports/api/chat"; +import { z } from "zod"; + +export const Chat = createModule("chat") + .addMethod("createChat", z.void(), async () => { + return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] }); + }) + .buildSubmodule(); +``` + +```typescript [client/main.ts] +import { createClient } from "grubba-rpc"; +// you must import the type of the server +import type { Server } from "/imports/api/server"; + +const api = createClient(); +const bar: "bar" = await api.bar("some string"); +// ?^ 'bar' +const newChatId = await api.chat.createChat(); // with intellisense +``` + +::: + +### `module.addMethod` + +Type: + +```ts +addMethod( + name: string, + schema: ZodSchema, + handler: (args: ZodTypeInput) => T, + config?: Config, T> +) +``` + +This is the equivalent of `Meteor.methods` but with types and runtime validation. + +::: code-group + +```typescript [server/with-meteor-rpc.ts] +import { createModule } from "grubba-rpc"; +import { z } from "zod"; + +const server = createModule() + .addMethod("foo", z.string(), (arg) => "foo" as const) + .build(); +``` + +```typescript [server/without-meteor-rpc.ts] +import { Meteor } from "meteor/meteor"; +import { z } from "zod"; + +Meteor.methods({ + foo(arg: string) { + z.string().parse(arg); + return "foo"; + }, +}); +``` + +::: + +### `module.addPublication` + +Type: + +```typescript +addPublication( + name: string, + schema: ZodSchema, + handler: (args: ZodTypeInput) => Cursor +) +``` + +This is the equivalent of `Meteor.publish` but with types and runtime validation. + +::: code-group + +```typescript [server/with-meteor-rpc.ts] +import { createModule } from "grubba-rpc"; +import { ChatCollection } from "/imports/api/chat"; +import { z } from "zod"; + +const server = createModule() + .addPublication("chatRooms", z.void(), () => { + return ChatCollection.find(); + }) + .build(); +``` + +```typescript [server/without-meteor-rpc.ts] +import { Meteor } from "meteor/meteor"; +import { ChatCollection } from "/imports/api/chat"; + +Meteor.publish("chatRooms", function () { + return ChatCollection.find(); +}); +``` + +::: + +### `module.addSubmodule` + +This is used to add a submodule to the main module, adding namespaces for your methods and publications and making it easier to organize your code. + +> Remember to use `submodule.buildSubmodule` when creating a submodule + +::: code-group + +```typescript [server/chat.ts] +import { ChatCollection } from "/imports/api/chat"; +import { createModule } from "grubba-rpc"; + +export const chatModule = createModule("chat") + .addMethod("createChat", z.void(), async () => { + return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] }); + }) + .buildSubmodule(); // <-- this is important so that this module can be added as a submodule +``` + +```typescript [server/chat.ts] +import { createModule } from "grubba-rpc"; +import { chatModule } from "./server/chat"; + +const server = createModule() + .addMethod("bar", z.string(), (arg) => "bar" as const) + .addSubmodule(chatModule) + .build(); + +server.chat; // <-- this is the namespace for the chat module +server.chat.createChat(); // <-- this is the method from the chat module and it gets autocompleted +``` + +::: + +### `module.addMiddlewares` + +Type: + +```typescript +type Middleware = (raw: unknown, parsed: unknown) => void; + +addMiddlewares(middlewares: Middleware[]) +``` + +This is used to add middlewares to the module, it can be used to add side effects logic to the methods and publications, ideal for logging or rate limiting. + +The middleware ordering is last in first out. Check the example below: + +::: code-group + +```typescript [server/chat.ts] +import { ChatCollection } from "/imports/api/chat"; +import { createModule } from "grubba-rpc"; + +export const chatModule = createModule("chat") + .addMiddlewares([ + (raw, parsed) => { + console.log("runs first"); + }, + ]) + .addMethod("createChat", z.void(), async () => { + return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] }); + }) + .buildSubmodule(); +``` + +```typescript [server/main.ts] +import { createModule } from "grubba-rpc"; +import { chatModule } from "./server/chat"; + +const server = createModule() + .addMiddlewares([ + (raw, parsed) => { + console.log("runs second"); + }, + ]) + .addMethod("bar", z.string(), (arg) => "bar" as const) + .addSubmodule(chatModule) + .build(); +``` + +```typescript [client/main.ts] +import { createClient } from "grubba-rpc"; +import type { Server } from "/imports/api/server"; // you must import the type + +const api = createClient(); +await api.chat.createChat(); // logs "runs first" then "runs second" +await api.bar("str"); // logs "runs second" +``` + +::: + +### `module.build` + +This is used to build the module, it should be used at the end of the module creation to ensure that the exported type is correct. + +::: code-group + +```typescript [correct.ts] +// ✅ it has the build method +import { createModule } from "grubba-rpc"; +import { z } from "zod"; +const server = createModule() + .addMethod("bar", z.string(), (arg) => "bar" as const) + .build(); + +export type Server = typeof server; +``` + +```typescript [incorrect.ts] +// ❌ it is missing the build method +import { createModule } from "grubba-rpc"; +import { z } from "zod"; +const server = createModule().addMethod( + "bar", + z.string(), + (arg) => "bar" as const +); + +export type Server = typeof server; +``` + +::: + +### `module.buildSubmodule` + +This is used to build the submodule, it should be used at the end of the submodule creation and imported in the main module in the [`addSubmodule`](./meteor-rpc.md#module-addsubmodule) method. + +::: code-group + +```typescript [correct.ts] +import { createModule } from "grubba-rpc"; +import { z } from "zod"; + +export const chatModule = createModule("chat") + .addMethod("createChat", z.void(), async () => { + return "chat" as const; + }) + // ✅ it has the buildSubmodule method + .buildSubmodule(); +``` + +```typescript [incorrect.ts] +import { createModule } from "grubba-rpc"; +import { z } from "zod"; + +export const otherSubmodule = createModule("other") + .addMethod("otherMethod", z.void(), async () => { + return "other" as const; + }) + // ❌ it is missing the buildSubmodule method + .build(); + +export const otherSubmodule = createModule("other").addMethod( + "otherMethod", + z.void(), + async () => { + return "other" as const; + } +); // ❌ it is missing the buildSubmodule method +``` + +```typescript [server/main.ts] +import { createModule } from "grubba-rpc"; +import { chatModule } from "./server/chat"; + +const server = createModule() + .addMethod("bar", z.string(), (arg) => "bar" as const) + .addSubmodule(chatModule) + .build(); +``` + +::: + +## Using in the client + +When using in the client you _have_ to use the `createModule` and `build` methods to create a module that will be used in the client +and be sure that you are exporting the type of the module + +_You should only create one client in your application_ + +You can have something like `api.ts` that will export the client and the type of the client + +::: code-group + +```typescript [server/main.ts] +import { createModule } from "grubba-rpc"; + +const server = createModule() + .addMethod("bar", z.string(), (arg) => "bar" as const) + .build(); + +export type Server = typeof server; +``` + +```typescript [client/main.ts] +// you must import the type +import type { Server } from "/imports/api/server"; +const app = createClient(); + +await app.bar("str"); // it will return "bar" +``` + +::: + +## React focused API + +Our package has a React focused API that uses `react-query` to handle the data fetching and mutations. + +### `method.useMutation` + +It uses the [`useMutation`](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) from react-query to create a mutation that will call the method + +::: code-group + +```typescript [server/main.ts] +import { createModule } from "grubba-rpc"; + +const server = createModule() + .addMethod("bar", z.string(), (arg) => { + console.log("Server received", arg); + return "bar" as const; + }) + .build(); + +export type Server = typeof server; +``` + +```tsx [client.ts] +// you must import the type +import type { Server } from "/imports/api/server"; +const app = createClient(); + +export const Component = () => { + const { mutate, isLoading, isError, error, data } = app.bar.useMutation(); + + return ( + + ); +}; +``` + +::: + +### `method.useQuery` + +It uses the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useSuspenseQuery#usesuspensequery) from react-query to create a query that will call the method, it uses `suspense` to handle loading states + +::: code-group + +```typescript [server/main.ts] +import { createModule } from "grubba-rpc"; + +const server = createModule() + .addMethod("bar", z.string(), (arg) => "bar" as const) + .build(); + +export type Server = typeof server; +``` + +```tsx [client.ts] +// you must import the type of the server +import type { Server } from "/imports/api/server"; +const app = createClient(); + +export const Component = () => { + const { data } = app.bar.useQuery("str"); // this will trigger suspense + + return
    {data}
    ; +}; +``` + +::: + +### `publication.useSubscription` + +Subscriptions on the client have `useSubscription` method that can be used as a hook to subscribe to a publication. It uses `suspense` to handle loading states + +::: code-group + +```typescript [server/main.ts] +// server/main.ts +import { createModule } from "grubba-rpc"; +import { ChatCollection } from "/imports/api/chat"; +import { z } from "zod"; + +const server = createModule() + .addPublication("chatRooms", z.void(), () => { + return ChatCollection.find(); + }) + .build(); + +export type Server = typeof server; +``` + +```tsx [client.ts] +import type { Server } from "/imports/api/server"; // you must import the type +const app = createClient(); + +export const Component = () => { + // it will trigger suspense and `rooms` is reactive in this context. + // When there is a change in the collection it will rerender + const { data: rooms, collection: chatCollection } = + api.chatRooms.usePublication(); + + return ( +
    + {rooms.map((room) => ( +
    {room.name}
    + ))} +
    + ); +}; +``` + +::: + +## Examples + +Currently we have: + +- [chat-app](https://github.com/Grubba27/testing-meteor-rpc) that uses this package to create a chat-app +- [askme](https://github.com/fredmaiaarantes/askme) that uses this package to create a Q&A app, you can check it live [here](https://askmeaquestion.meteorapp.com/) + +## Advanced usage + +you can take advantage of the hooks to add custom logic to your methods, checking the raw and parsed data, and the result of the method, +if the method has failed you can also check the error. + +::: code-group + +```typescript [on-method-after-creation.ts] +import { createModule } from "grubba-rpc"; +import { z } from "zod"; + +const server = createModule() + .addMethod("bar", z.string(), (arg) => "bar" as const) + .build(); + +// you can add hooks after the method has been created +server.bar.addBeforeResolveHook((raw, parsed) => { + console.log("before resolve", raw, parsed); +}); + +server.bar.addAfterResolveHook((raw, parsed, result) => { + console.log("after resolve", raw, parsed, result); +}); + +server.bar.addErrorResolveHook((err, raw, parsed) => { + console.log("on error", err, raw, parsed); +}); + +export type Server = typeof server; +``` + +```typescript [on-method-creation.ts] +import { createModule } from "grubba-rpc"; +import { z } from "zod"; + +const server = createModule() + // Or you can add hooks when creating the method + .addMethod("bar", z.any(), () => "str", { + hooks: { + onBeforeResolve: [ + (raw, parsed) => { + console.log("before resolve", raw, parsed); + }, + ], + onAfterResolve: [ + (raw, parsed, result) => { + console.log("after resolve", raw, parsed, result); + }, + ], + onErrorResolve: [ + (err, raw, parsed) => { + console.log("on error", err, raw, parsed); + }, + ], + }, + }) + .build(); + +export type Server = typeof server; +``` + +::: From bd9cc2cee4f0ed7a709d163cbbe939e9437942f6 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 2 Dec 2024 20:20:49 -0300 Subject: [PATCH 13/32] DOCS: add brief description of Meteor RPC to community packages --- v3-docs/docs/community-packages/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index 8c73cae2b0..3e2bd6d2fe 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -8,6 +8,7 @@ This section tries to list and add some information about usage, configuration, If you use or have a package that you think would be useful to add to this list, please open a pull request. Please bear in mind if you are adding a package to this list, try providing as much information as possible, including: + - `Who maintains the package` – how to get in touch to submit issues or questions - `Why is this package for?` - `API` @@ -17,4 +18,4 @@ Please bear in mind if you are adding a package to this list, try providing as m #### Method/Subscription helpers -- [`meteor-rpc`](./meteor-rpc.md) \ No newline at end of file +- [`meteor-rpc`](./meteor-rpc.md), Meteor Methods Evolved with type checking and runtime validation From 69c6882133198ed505d855012651b85682dff14a Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 2 Dec 2024 20:23:04 -0300 Subject: [PATCH 14/32] DOCS: add meteor-rpc to vite config links --- v3-docs/docs/.vitepress/config.mts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 5cf32b8890..4cd9c168f9 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -384,7 +384,12 @@ export default defineConfig({ { text: "Community Packages", link: "/community-packages/index", - items: [], + items: [ + { + text: "Meteor RPC", + link: "/community-packages/meteor-rpc", + }, + ], collapsed: true, }, { From 314167b246615156b3aeca118bf35eff547d6e4c Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Tue, 3 Dec 2024 09:37:57 -0300 Subject: [PATCH 15/32] DOCS: add warning about meteor-rpc compat with Meteor 2.8 and up --- v3-docs/docs/community-packages/meteor-rpc.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v3-docs/docs/community-packages/meteor-rpc.md b/v3-docs/docs/community-packages/meteor-rpc.md index 007746dc1f..1ab8072bce 100644 --- a/v3-docs/docs/community-packages/meteor-rpc.md +++ b/v3-docs/docs/community-packages/meteor-rpc.md @@ -12,6 +12,18 @@ This package provides functions for building E2E type-safe RPCs, focused on Reac ## How to download it? +::: warning + +This package works only with Meteor 2.8 or higher. + +If not sure about the version of Meteor you are using, you can check it by running the following command in your terminal within your project: + +```bash +meteor --version +``` + +::: + ```bash meteor npm i grubba-rpc @tanstack/react-query zod ``` From 91bc27f0da01ff1437a4bd7aa3c8ec702e329629 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Tue, 3 Dec 2024 19:00:38 -0300 Subject: [PATCH 16/32] DOCS: remove known issues tab it is no longer relevant for the current version of meteor. --- v3-docs/docs/.vitepress/config.mts | 4 -- v3-docs/docs/troubleshooting/known-issues.md | 39 -------------------- 2 files changed, 43 deletions(-) delete mode 100644 v3-docs/docs/troubleshooting/known-issues.md diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 5cf32b8890..3e36909b2a 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -395,10 +395,6 @@ export default defineConfig({ link: "/troubleshooting/expired-certificate", }, { text: "Windows", link: "/troubleshooting/windows" }, - { - text: "Known issues in 2.13", - link: "/troubleshooting/known-issues", - }, ], collapsed: true, }, diff --git a/v3-docs/docs/troubleshooting/known-issues.md b/v3-docs/docs/troubleshooting/known-issues.md deleted file mode 100644 index f533d80a76..0000000000 --- a/v3-docs/docs/troubleshooting/known-issues.md +++ /dev/null @@ -1,39 +0,0 @@ - -# Known issues in 2.13 - -Troubleshooting in Meteor 2.13 - -## Cannot extract version of meteor tool {#cannot-extract-meteor-tool} - - -For some users, the `meteor update` to version 2.13 command may fail with the following error or similar: - -```shell -Error: incorrect data check - at Zlib.zlibOnError [as onerror] (zlib.js:187:17) - => awaited here: - ... - at /tools/cli/main.js:1165:7 { - errno: -3, - code: 'Z_DATA_ERROR' - } - -``` - -## The issue {#the-issue} - -It seems related to [our first ESM version of Node.js v14.21.4](https://github.com/meteor/node-v14-esm) and the `zlib` package. -We have been able to reproduce this issue only in Mac Intel. - -You can follow along with the [GitHub issue](https://github.com/meteor/meteor/issues/12731) for updates. - -## Solution {#solution} - -The solution for this issue is running the following command in your terminal: - -```shell - -curl https://install.meteor.com/\?release\=2.13.3 | sh - -``` - From 6a0db09fc468059202dd1e5d8184ed73e0a488a7 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Tue, 3 Dec 2024 19:38:27 -0300 Subject: [PATCH 17/32] DOCS: remove comments from docs --- v3-docs/docs/community-packages/meteor-rpc.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/v3-docs/docs/community-packages/meteor-rpc.md b/v3-docs/docs/community-packages/meteor-rpc.md index 1ab8072bce..e97a6e99b5 100644 --- a/v3-docs/docs/community-packages/meteor-rpc.md +++ b/v3-docs/docs/community-packages/meteor-rpc.md @@ -38,18 +38,6 @@ Before continuing the installation make sure you have `react-query` all set in y There is a few concepts that are important while using this package: - - - This package is built on top of [`Meteor.methods`](../api/meteor.md#method-apis-methods) and [`Meteor.publish`](../api/meteor.md#publish-and-subscribe-pubsub) but with types and runtime validation, their understanding is important to use this package. - Every method and publication use `zod` to validate the arguments, so you can be sure that the data you are receiving is the data you are expecting. From 270a864e8a15b8a0672883e13fdd3c453764447d Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Wed, 4 Dec 2024 09:59:37 -0300 Subject: [PATCH 18/32] DOCS: move community packages to section --- v3-docs/docs/community-packages/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index 3c23fad224..8ab9b502b0 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -1,13 +1,22 @@ # Community Packages +::: tip +The Meteor community is making great efforts to migrate popular packages to Meteor 3.0. + +A [spreadsheet maintained](https://docs.google.com/spreadsheets/u/0/d/1JbUZmJab3owZ9LV71Ubto32YX_QWQljRypJTOQupxL8/htmlview) by [@harryadel](https://github.com/harryadel) tracks the status of your favorite packages and offers opportunities to help. +::: + There are some very popular community packages that do not have a documentation website or only have a readme file. This section tries to list and add some information about usage, configuration, and examples for these packages. > Think this section as an `Awesome List`, similar to [`awesome-node`](https://github.com/sindresorhus/awesome-nodejs) or [`awesome-react`](https://github.com/enaqx/awesome-react) +Many packages have been consolidated into the [Meteor Community organization](https://github.com/Meteor-Community-Packages). Others are maintained by individual developers or companies. + If you use or have a package that you think would be useful to add to this list, please open a pull request. Please bear in mind if you are adding a package to this list, try providing as much information as possible, including: + - `Who maintains the package` – how to get in touch to submit issues or questions - `Why is this package for?` - `API` From fdec6d735117828577a9297ded16b76a1ffe8594 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Wed, 4 Dec 2024 10:01:07 -0300 Subject: [PATCH 19/32] DOCS: remove old references to community packages --- v3-docs/docs/.vitepress/config.mts | 4 ---- v3-docs/docs/packages/community-packages.md | 17 ----------------- 2 files changed, 21 deletions(-) delete mode 100644 v3-docs/docs/packages/community-packages.md diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 3e36909b2a..7bd70ba18e 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -374,10 +374,6 @@ export default defineConfig({ link: "/packages/packages-listing", text: "Maintained Packages", }, - { - link: "packages/community-packages", - text: "Community Packages", - }, ], collapsed: true, }, diff --git a/v3-docs/docs/packages/community-packages.md b/v3-docs/docs/packages/community-packages.md deleted file mode 100644 index 626a7e871e..0000000000 --- a/v3-docs/docs/packages/community-packages.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[//]: # (Do not edit this file by hand.) - -[//]: # (This is a generated file.) - -[//]: # (If you want to change something in this file) - -[//]: # (go to meteor/docs/generators/packages-listing) - -# Community Packages - -The Meteor community is making great efforts to migrate popular packages to Meteor 3.0. - -Many packages have been consolidated into the [Meteor Community organization](https://github.com/Meteor-Community-Packages). Others are maintained by individual developers or companies. - -A [spreadsheet maintained](https://docs.google.com/spreadsheets/u/0/d/1JbUZmJab3owZ9LV71Ubto32YX_QWQljRypJTOQupxL8/htmlview) by [@harryadel](https://github.com/harryadel) tracks the status of your favorite packages and offers opportunities to help. From 0a8db7532038ff836c1bc06a2dc3c87a906e1437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 5 Dec 2024 14:26:03 +0100 Subject: [PATCH 20/32] fix flaky tests by comparing expected array values equality regardless the order --- packages/roles/tests/serverAsync.js | 62 +++++++++++++++++------------ 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/roles/tests/serverAsync.js b/packages/roles/tests/serverAsync.js index 1d207adeea..2c403228c6 100644 --- a/packages/roles/tests/serverAsync.js +++ b/packages/roles/tests/serverAsync.js @@ -57,23 +57,23 @@ const sameMembers = (test, value, expected) => { ); }; -const sameDeepMembers = (test, value, expected) => { - // Helper to sort object keys recursively - const sortObjectKeys = (obj) => { - if (Array.isArray(obj)) { - return obj.map(sortObjectKeys); - } - if (obj && typeof obj === "object") { - return Object.keys(obj) - .sort() - .reduce((sorted, key) => { - sorted[key] = sortObjectKeys(obj[key]); - return sorted; - }, {}); - } - return obj; - }; +// Helper to sort object keys recursively +const sortObjectKeys = (obj) => { + if (Array.isArray(obj)) { + return obj.map(sortObjectKeys); + } + if (obj && typeof obj === "object") { + return Object.keys(obj) + .sort() + .reduce((sorted, key) => { + sorted[key] = sortObjectKeys(obj[key]); + return sorted; + }, {}); + } + return obj; +}; +const sameDeepMembers = (test, value, expected) => { const sortedValue = sortObjectKeys(value); const sortedExpected = sortObjectKeys(expected); @@ -84,6 +84,16 @@ const sameDeepMembers = (test, value, expected) => { ); }; +function sameDeepUnorderedMembers(test, value, expected) { + const sortAndStringify = (arr) => { + return JSON.stringify(arr.map(item => JSON.stringify(sortObjectKeys(item))).sort()); + }; + const sortedValue = sortAndStringify(value); + const sortedExpected = sortAndStringify(expected); + + test.equal(sortedValue, sortedExpected, 'Arrays should have the same elements, regardless of order'); +} + const hasProp = (target, prop) => Object.hasOwnProperty.call(target, prop); let users = {}; @@ -1885,7 +1895,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser.map((obj) => { delete obj._id; @@ -1914,7 +1924,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser2.map((obj) => { delete obj._id; @@ -1949,7 +1959,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser3.map((obj) => { delete obj._id; @@ -1973,7 +1983,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser4.map((obj) => { delete obj._id; @@ -2063,7 +2073,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles.map((obj) => { delete obj._id; @@ -2109,7 +2119,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles2.map((obj) => { delete obj._id; @@ -2153,7 +2163,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles3.map((obj) => { delete obj._id; @@ -2203,7 +2213,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles4.map((obj) => { delete obj._id; @@ -2255,7 +2265,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles5.map((obj) => { delete obj._id; @@ -2308,7 +2318,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles6.map((obj) => { delete obj._id; From 197ea3e00403e7edb78f7b20ee0123c877c262bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 5 Dec 2024 14:41:11 +0100 Subject: [PATCH 21/32] convert to arrow func to perserve code style --- packages/roles/tests/serverAsync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roles/tests/serverAsync.js b/packages/roles/tests/serverAsync.js index 2c403228c6..1e844550fc 100644 --- a/packages/roles/tests/serverAsync.js +++ b/packages/roles/tests/serverAsync.js @@ -84,7 +84,7 @@ const sameDeepMembers = (test, value, expected) => { ); }; -function sameDeepUnorderedMembers(test, value, expected) { +const sameDeepUnorderedMembers = (test, value, expected) => { const sortAndStringify = (arr) => { return JSON.stringify(arr.map(item => JSON.stringify(sortObjectKeys(item))).sort()); }; @@ -92,7 +92,7 @@ function sameDeepUnorderedMembers(test, value, expected) { const sortedExpected = sortAndStringify(expected); test.equal(sortedValue, sortedExpected, 'Arrays should have the same elements, regardless of order'); -} +}; const hasProp = (target, prop) => Object.hasOwnProperty.call(target, prop); From 82b730e7a09d5e159a205be80af800471f1a2287 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 6 Dec 2024 10:03:23 -0300 Subject: [PATCH 22/32] DOCS: move roles docs to packages folder --- v3-docs/docs/.vitepress/config.mts | 4 ++++ v3-docs/docs/{api => packages}/roles.md | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) rename v3-docs/docs/{api => packages}/roles.md (98%) diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index fa04db0aca..322e37ef3d 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -318,6 +318,10 @@ export default defineConfig({ text: "fetch", link: "/packages/fetch", }, + { + text: "roles", + link: "/packages/roles", + }, { text: "hot-module-replacement", link: "/packages/hot-module-replacement", diff --git a/v3-docs/docs/api/roles.md b/v3-docs/docs/packages/roles.md similarity index 98% rename from v3-docs/docs/api/roles.md rename to v3-docs/docs/packages/roles.md index fdaee6c473..526b2d6fef 100644 --- a/v3-docs/docs/api/roles.md +++ b/v3-docs/docs/packages/roles.md @@ -214,7 +214,7 @@ Example: const roles = Roles.getAllRoles({ sort: { _id: 1 } }); // Get roles with custom query -const customRoles = Roles.getAllRoles({ +const customRoles = Roles.getAllRoles({ fields: { _id: 1, children: 1 }, sort: { _id: -1 } }); @@ -234,7 +234,7 @@ const scopedUsers = await Roles.getUsersInRoleAsync(['editor', 'writer'], 'blog' // Find users with custom options const users = await Roles.getUsersInRoleAsync('manager', { scope: 'department-a', - queryOptions: { + queryOptions: { sort: { createdAt: -1 }, limit: 10 } @@ -370,6 +370,10 @@ If you are currently using the `alanning:roles` package, follow these steps to m ```bash meteor add roles ``` +6. Update imports to use the new package: + ```js + import { Roles } from "meteor/roles"; + ``` The sync versions of these functions are still available on the client. @@ -450,5 +454,3 @@ Meteor.methods({ }, }); ``` - - From eb4b7bafd0d825703eb79fbde06a2dcc0095ea73 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 6 Dec 2024 10:09:37 -0300 Subject: [PATCH 23/32] DOCS: merge from devel config --- v3-docs/docs/.vitepress/config.mts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 322e37ef3d..eab9c52d87 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -350,6 +350,10 @@ export default defineConfig({ text: "random", link: "/packages/random", }, + { + text: "react-meteor-data", + link: "/packages/react-meteor-data", + }, { text: "server-render", link: "/packages/server-render", @@ -374,13 +378,15 @@ export default defineConfig({ link: "/packages/packages-listing", text: "Maintained Packages", }, - { - link: "packages/community-packages", - text: "Community Packages", - }, ], collapsed: true, }, + { + text: "Community Packages", + link: "/community-packages/index", + items: [], + collapsed: true, + }, { text: "Troubleshooting", items: [ @@ -389,10 +395,6 @@ export default defineConfig({ link: "/troubleshooting/expired-certificate", }, { text: "Windows", link: "/troubleshooting/windows" }, - { - text: "Known issues in 2.13", - link: "/troubleshooting/known-issues", - }, ], collapsed: true, }, From 17aa93549fca575a2e906fcd11f01cbce8d04699 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 6 Dec 2024 10:14:35 -0300 Subject: [PATCH 24/32] docs: add roles again in config --- v3-docs/docs/.vitepress/config.mts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index ebb0235aea..1fdc607a2e 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -322,6 +322,10 @@ export default defineConfig({ text: "hot-module-replacement", link: "/packages/hot-module-replacement", }, + { + text: "roles", + link: "/packages/roles", + }, { text: "less", link: "/packages/less", From 59a64e46bc54dd4ccada25facb8924eda2cb10a2 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 6 Dec 2024 11:10:47 -0300 Subject: [PATCH 25/32] DOCS: add client sync methods --- v3-docs/docs/packages/roles.md | 50 +++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/v3-docs/docs/packages/roles.md b/v3-docs/docs/packages/roles.md index 526b2d6fef..4461c2dbec 100644 --- a/v3-docs/docs/packages/roles.md +++ b/v3-docs/docs/packages/roles.md @@ -137,7 +137,7 @@ Example: ```js // Rename an existing role -await Roles.renameRoleAsync('editor', 'content-editor'); +await Roles.renameRoleAsync("editor", "content-editor"); ``` ### Assigning Roles @@ -193,7 +193,7 @@ Example: ```js // Rename a scope -await Roles.renameScopeAsync('department-1', 'marketing'); +await Roles.renameScopeAsync("department-1", "marketing"); ``` @@ -202,7 +202,7 @@ Example: ```js // Remove a scope and all its role assignments -await Roles.removeScopeAsync('old-department'); +await Roles.removeScopeAsync("old-department"); ``` @@ -216,7 +216,7 @@ const roles = Roles.getAllRoles({ sort: { _id: 1 } }); // Get roles with custom query const customRoles = Roles.getAllRoles({ fields: { _id: 1, children: 1 }, - sort: { _id: -1 } + sort: { _id: -1 }, }); ``` @@ -226,18 +226,21 @@ Example: ```js // Find all admin users -const adminUsers = await Roles.getUsersInRoleAsync('admin'); +const adminUsers = await Roles.getUsersInRoleAsync("admin"); // Find users with specific roles in a scope -const scopedUsers = await Roles.getUsersInRoleAsync(['editor', 'writer'], 'blog'); +const scopedUsers = await Roles.getUsersInRoleAsync( + ["editor", "writer"], + "blog" +); // Find users with custom options -const users = await Roles.getUsersInRoleAsync('manager', { - scope: 'department-a', +const users = await Roles.getUsersInRoleAsync("manager", { + scope: "department-a", queryOptions: { sort: { createdAt: -1 }, - limit: 10 - } + limit: 10, + }, }); ``` @@ -291,10 +294,10 @@ Example: ```js // Check if admin is a parent of editor -const isParent = await Roles.isParentOfAsync('admin', 'editor'); +const isParent = await Roles.isParentOfAsync("admin", "editor"); // Can be used to check inheritance chains -const hasPermission = await Roles.isParentOfAsync('super-admin', 'post-edit'); +const hasPermission = await Roles.isParentOfAsync("super-admin", "post-edit"); ``` @@ -306,7 +309,7 @@ Example: const allScopes = await Roles.getScopesForUserAsync(userId); // Get scopes where user has specific roles -const editorScopes = await Roles.getScopesForUserAsync(userId, ['editor']); +const editorScopes = await Roles.getScopesForUserAsync(userId, ["editor"]); ``` ## Publishing Roles @@ -331,6 +334,27 @@ Meteor.publish("scopeRoles", function (scope) { }); ``` +## Client only APIs + +On the client alongside the async methods, you can use the `sync` versions of the functions: + +- `Roles.userIsInRole(userId, roles, scope)` +- `Roles.getRolesForUser(userId, scope)` +- `Roles.getScopesForUser(userId)` +- `Roles.isParentOf(parent, child)` +- `Roles.getUsersInRole(role, scope)` +- `Roles.getAllRoles(options)` +- `Roles.createRole(role, options)` +- `Roles.addUsersToRoles(userId, roles, scope)` +- `Roles.setUserRoles(userId, roles, scope)` +- `Roles.removeUsersFromRoles(userId, roles, scope)` +- `Roles.addRolesToParent(child, parent)` +- `Roles.removeRolesFromParent(child, parent)` +- `Roles.deleteRole(role)` +- `Roles.renameRole(oldRole, newRole)` +- `Roles.renameScope(oldScope, newScope)` +- `Roles.removeScope(scope)` + ## Using with Templates The roles package automatically provides an `isInRole` helper for templates: From 45459bd72981b25048749b28ca560a42c5d9800e Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 6 Dec 2024 11:15:16 -0300 Subject: [PATCH 26/32] FIX: Add missing types to roles package it was missing an `addAssets` in the `package.js` file --- packages/roles/package.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/roles/package.js b/packages/roles/package.js index 2ff58f3dee..6bbafa487f 100644 --- a/packages/roles/package.js +++ b/packages/roles/package.js @@ -25,17 +25,14 @@ Package.onUse(function (api) { api.addFiles("roles_common_async.js", both); api.addFiles("roles_server.js", "server"); api.addFiles(["client/debug.js", "client/uiHelpers.js"], "client"); + + api.addAssets("definitions.d.ts", both); }); Package.onTest(function (api) { const both = ["client", "server"]; - api.use([ - "tinytest", - "ecmascript", - "mongo", - "roles" - ], both); + api.use(["tinytest", "ecmascript", "mongo", "roles"], both); api.addFiles("tests/serverAsync.js", "server"); api.addFiles("tests/client.js", "client"); From 803e6d2eef506371bb78f4d1f2f9a0e673c2ba5f Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 9 Dec 2024 10:48:52 -0300 Subject: [PATCH 27/32] DEV: removed zodern:types explict dependency added package-types.json to roles package --- packages/roles/package-types.json | 3 +++ packages/roles/package.js | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 packages/roles/package-types.json diff --git a/packages/roles/package-types.json b/packages/roles/package-types.json new file mode 100644 index 0000000000..7838991b97 --- /dev/null +++ b/packages/roles/package-types.json @@ -0,0 +1,3 @@ +{ + "typesEntry": "definitions.d.ts" +} diff --git a/packages/roles/package.js b/packages/roles/package.js index 6bbafa487f..e3a7080835 100644 --- a/packages/roles/package.js +++ b/packages/roles/package.js @@ -15,8 +15,6 @@ Package.onUse(function (api) { both ); - api.use("zodern:types@1.0.13"); - api.use(["blaze@2.9.0 || 3.0.0"], "client", { weak: true }); api.export(["Roles", "RolesCollection", "RoleAssignmentCollection"]); @@ -26,7 +24,8 @@ Package.onUse(function (api) { api.addFiles("roles_server.js", "server"); api.addFiles(["client/debug.js", "client/uiHelpers.js"], "client"); - api.addAssets("definitions.d.ts", both); + api.addAssets("definitions.d.ts", "server"); + api.addAssets("package-types.json", "server"); }); Package.onTest(function (api) { From f1121457f9e457bcc4e467f4caa3ad39bb0294d3 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 9 Dec 2024 11:24:04 -0300 Subject: [PATCH 28/32] DEV: fix declarations --- packages/roles/definitions.d.ts | 232 ++++++++++++++++++++------------ 1 file changed, 149 insertions(+), 83 deletions(-) diff --git a/packages/roles/definitions.d.ts b/packages/roles/definitions.d.ts index d8f4441022..c15c8c9652 100644 --- a/packages/roles/definitions.d.ts +++ b/packages/roles/definitions.d.ts @@ -4,14 +4,14 @@ // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // Minimum TypeScript Version: 4.1 -import { Mongo } from 'meteor/mongo' +import { Mongo } from "meteor/mongo"; /** * Provides functions related to user authorization. Compatible with built-in Meteor accounts packages. * * @module Roles */ -declare namespace Roles { +export declare namespace Roles { /** * Constant used to reference the special 'global' group that * can be used to apply blanket permissions across all groups. @@ -27,7 +27,7 @@ declare namespace Roles { * @static * @final */ - var GLOBAL_GROUP: string + var GLOBAL_GROUP: string; /** * Subscription handle for the currently logged in user's permissions. @@ -42,7 +42,7 @@ declare namespace Roles { * * @for Roles */ - var subscription: Subscription + var subscription: Subscription; /** * Add users to roles. @@ -68,12 +68,12 @@ declare namespace Roles { users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; ifExists?: boolean } - ): void + ): void; function addUsersToRolesAsync( users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; ifExists?: boolean } - ): Promise + ): Promise; /** * Create a new role. @@ -84,8 +84,14 @@ declare namespace Roles { * - `unlessExists`: if `true`, exception will not be thrown in the role already exists * @return {String} ID of the new role or null. */ - function createRole(roleName: string, options?: { unlessExists: boolean }): string - function createRoleAsync(roleName: string, options?: { unlessExists: boolean }): Promise + function createRole( + roleName: string, + options?: { unlessExists: boolean } + ): string; + function createRoleAsync( + roleName: string, + options?: { unlessExists: boolean } + ): Promise; /** * Delete an existing role. @@ -95,8 +101,8 @@ declare namespace Roles { * @method deleteRole * @param {String} roleName Name of role. */ - function deleteRole(roleName: string): void - function deleteRoleAsync(roleName: string): Promise + function deleteRole(roleName: string): void; + function deleteRoleAsync(roleName: string): Promise; /** * Rename an existing role. @@ -105,8 +111,8 @@ declare namespace Roles { * @param {String} oldName Old name of a role. * @param {String} newName New name of a role. */ - function renameRole(oldName: string, newName: string): void - function renameRoleAsync(oldName: string, newName: string): Promise + function renameRole(oldName: string, newName: string): void; + function renameRoleAsync(oldName: string, newName: string): Promise; /** * Add role parent to roles. @@ -118,8 +124,14 @@ declare namespace Roles { * @param {Array|String} rolesNames Name(s) of role(s). * @param {String} parentName Name of parent role. */ - function addRolesToParent(rolesNames: string | string[], parentName: string): void - function addRolesToParentAsync(rolesNames: string | string[], parentName: string): Promise + function addRolesToParent( + rolesNames: string | string[], + parentName: string + ): void; + function addRolesToParentAsync( + rolesNames: string | string[], + parentName: string + ): Promise; /** * Remove role parent from roles. @@ -131,8 +143,14 @@ declare namespace Roles { * @param {Array|String} rolesNames Name(s) of role(s). * @param {String} parentName Name of parent role. */ - function removeRolesFromParent(rolesNames: string | string[], parentName: string): void - function removeRolesFromParentAsync(rolesNames: string | string[], parentName: string): Promise + function removeRolesFromParent( + rolesNames: string | string[], + parentName: string + ): void; + function removeRolesFromParentAsync( + rolesNames: string | string[], + parentName: string + ): Promise; /** * Retrieve cursor of all existing roles. @@ -142,7 +160,7 @@ declare namespace Roles { * through to `Meteor.roles.find(query, options)`. * @return {Cursor} Cursor of existing roles. */ - function getAllRoles(queryOptions?: QueryOptions): Mongo.Cursor + function getAllRoles(queryOptions?: QueryOptions): Mongo.Cursor; /** * Retrieve users groups, if any @@ -153,8 +171,14 @@ declare namespace Roles { * * @return {Array} Array of user's groups, unsorted. Roles.GLOBAL_GROUP will be omitted */ - function getGroupsForUser(user: string | Meteor.User, role?: string): string[] - function getGroupsForUserAsync(user: string | Meteor.User, role?: string): Promise + function getGroupsForUser( + user: string | Meteor.User, + role?: string + ): string[]; + function getGroupsForUserAsync( + user: string | Meteor.User, + role?: string + ): Promise; /** * Retrieve users scopes, if any. @@ -165,8 +189,14 @@ declare namespace Roles { * * @return {Array} Array of user's scopes, unsorted. */ - function getScopesForUser(user: string | Meteor.User, roles?: string | string[]): string[] - function getScopesForUserAsync(user: string | Meteor.User, roles?: string | string[]): Promise + function getScopesForUser( + user: string | Meteor.User, + roles?: string | string[] + ): string[]; + function getScopesForUserAsync( + user: string | Meteor.User, + roles?: string | string[] + ): Promise; /** * Rename a scope. @@ -177,8 +207,8 @@ declare namespace Roles { * @param {String} oldName Old name of a scope. * @param {String} newName New name of a scope. */ - function renameScope(oldName: string, newName: string): void - function renameScopeAsync(oldName: string, newName: string): Promise + function renameScope(oldName: string, newName: string): void; + function renameScopeAsync(oldName: string, newName: string): Promise; /** * Remove a scope. @@ -189,8 +219,8 @@ declare namespace Roles { * @param {String} name The name of a scope. * */ - function removeScope(name: String): void - function removeScopeAsync(name: String): Promise + function removeScope(name: String): void; + function removeScopeAsync(name: String): Promise; /** * Find out if a role is an ancestor of another role. @@ -202,8 +232,11 @@ declare namespace Roles { * @param {String} childRoleName The role you expect to be among the children of parentRoleName. * @return {Boolean} */ - function isParentOf(parentRoleName: string, childRoleName: string): boolean - function isParentOfAsync(parentRoleName: string, childRoleName: string): Promise + function isParentOf(parentRoleName: string, childRoleName: string): boolean; + function isParentOfAsync( + parentRoleName: string, + childRoleName: string + ): Promise; /** * Retrieve user's roles. @@ -222,20 +255,30 @@ declare namespace Roles { * Alternatively, it can be a scope name string. * @return {Array} Array of user's roles, unsorted. */ - function getRolesForUser(user: string | Meteor.User, options?: string | { - scope?: string; - anyScope?: boolean; - onlyScoped?: boolean; - onlyAssigned?: boolean; - fullObjects?: boolean - }): string[] - function getRolesForUserAsync(user: string | Meteor.User, options?: string | { - scope?: string; - anyScope?: boolean; - onlyScoped?: boolean; - onlyAssigned?: boolean; - fullObjects?: boolean - }): Promise + function getRolesForUser( + user: string | Meteor.User, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + onlyAssigned?: boolean; + fullObjects?: boolean; + } + ): string[]; + function getRolesForUserAsync( + user: string | Meteor.User, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + onlyAssigned?: boolean; + fullObjects?: boolean; + } + ): Promise; /** * Retrieve all assignments of a user which are for the target role. @@ -257,11 +300,16 @@ declare namespace Roles { * Alternatively, it can be a scope name string. * @return {Cursor} Cursor of user assignments for roles. */ - function getUserAssignmentsForRole(roles: string | string[], options?: string | { - scope?: string - anyScope?: boolean - queryOptions?: QueryOptions - }): Mongo.Cursor + function getUserAssignmentsForRole( + roles: string | string[], + options?: + | string + | { + scope?: string; + anyScope?: boolean; + queryOptions?: QueryOptions; + } + ): Mongo.Cursor; /** * Retrieve all users who are in target role. @@ -288,14 +336,28 @@ declare namespace Roles { */ function getUsersInRole( roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; onlyScoped?: boolean; queryOptions?: QueryOptions }, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + queryOptions?: QueryOptions; + }, queryOptions?: QueryOptions - ): Mongo.Cursor + ): Mongo.Cursor; function getUsersInRoleAsync( roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; onlyScoped?: boolean; queryOptions?: QueryOptions }, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + queryOptions?: QueryOptions; + }, queryOptions?: QueryOptions - ): Promise> + ): Promise>; /** * Remove users from assigned roles. @@ -318,12 +380,12 @@ declare namespace Roles { users: string | string[] | Meteor.User | Meteor.User[], roles?: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): void + ): void; function removeUsersFromRolesAsync( users: string | string[] | Meteor.User | Meteor.User[], roles?: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): Promise + ): Promise; /** * Set users' roles. @@ -349,13 +411,17 @@ declare namespace Roles { function setUserRoles( users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; ifExists?: boolean } - ): void + options?: + | string + | { scope?: string; anyScope?: boolean; ifExists?: boolean } + ): void; function setUserRolesAsync( users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; ifExists?: boolean } - ): Promise + options?: + | string + | { scope?: string; anyScope?: boolean; ifExists?: boolean } + ): Promise; /** * Check if user has specified roles. @@ -389,55 +455,55 @@ declare namespace Roles { user: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): boolean + ): boolean; function userIsInRoleAsync( user: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): Promise + ): Promise; // The schema for the roles collection interface Role { - _id: string - name: string - children: { _id: string }[] + _id: string; + name: string; + children: { _id: string }[]; } // The schema for the role-assignment collection interface RoleAssignment { - _id: string + _id: string; user: { - _id: string - } + _id: string; + }; role: { - _id: string - } + _id: string; + }; inheritedRoles?: { - _id: string - }[] - scope?: string + _id: string; + }[]; + scope?: string; } interface QueryOptions { - sort?: Mongo.SortSpecifier | undefined - skip?: number | undefined - limit?: number | undefined - fields?: Mongo.FieldSpecifier | undefined - projection?: Mongo.FieldSpecifier | undefined - reactive?: boolean | undefined - transform?: Function | undefined + sort?: Mongo.SortSpecifier | undefined; + skip?: number | undefined; + limit?: number | undefined; + fields?: Mongo.FieldSpecifier | undefined; + projection?: Mongo.FieldSpecifier | undefined; + reactive?: boolean | undefined; + transform?: Function | undefined; } - } // module // Exported collections -declare type RolesCollection = Mongo.Collection -declare type RoleAssignmentsCollection = Mongo.Collection +export declare type RolesCollection = Mongo.Collection; +export declare type RoleAssignmentsCollection = + Mongo.Collection; // Additions to the Meteor object -declare module 'meteor/meteor' { - namespace Meteor { - const roles: Mongo.Collection - const roleAssignment: Mongo.Collection +declare module "meteor/meteor" { + export namespace Meteor { + const roles: Mongo.Collection; + const roleAssignment: Mongo.Collection; } } From 8a0f0b709a6ae09395bd4b76770bd95640cb38d3 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 13 Dec 2024 09:54:06 -0300 Subject: [PATCH 29/32] DOCS: rename to meteor-rpc and add known issues part --- v3-docs/docs/community-packages/meteor-rpc.md | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/v3-docs/docs/community-packages/meteor-rpc.md b/v3-docs/docs/community-packages/meteor-rpc.md index e97a6e99b5..a830f5f9aa 100644 --- a/v3-docs/docs/community-packages/meteor-rpc.md +++ b/v3-docs/docs/community-packages/meteor-rpc.md @@ -25,7 +25,7 @@ meteor --version ::: ```bash -meteor npm i grubba-rpc @tanstack/react-query zod +meteor npm i meteor-rpc @tanstack/react-query zod ``` ::: warning @@ -60,7 +60,7 @@ Example: ::: code-group ```typescript [server/main.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { Chat } from "./chat"; const server = createModule() // server has no namespace @@ -72,7 +72,7 @@ export type Server = typeof server; ``` ```typescript [server/chat.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { ChatCollection } from "/imports/api/chat"; import { z } from "zod"; @@ -84,7 +84,7 @@ export const Chat = createModule("chat") ``` ```typescript [client/main.ts] -import { createClient } from "grubba-rpc"; +import { createClient } from "meteor-rpc"; // you must import the type of the server import type { Server } from "/imports/api/server"; @@ -114,7 +114,7 @@ This is the equivalent of `Meteor.methods` but with types and runtime validation ::: code-group ```typescript [server/with-meteor-rpc.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; const server = createModule() @@ -153,7 +153,7 @@ This is the equivalent of `Meteor.publish` but with types and runtime validation ::: code-group ```typescript [server/with-meteor-rpc.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { ChatCollection } from "/imports/api/chat"; import { z } from "zod"; @@ -185,7 +185,7 @@ This is used to add a submodule to the main module, adding namespaces for your m ```typescript [server/chat.ts] import { ChatCollection } from "/imports/api/chat"; -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; export const chatModule = createModule("chat") .addMethod("createChat", z.void(), async () => { @@ -195,7 +195,7 @@ export const chatModule = createModule("chat") ``` ```typescript [server/chat.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { chatModule } from "./server/chat"; const server = createModule() @@ -227,7 +227,7 @@ The middleware ordering is last in first out. Check the example below: ```typescript [server/chat.ts] import { ChatCollection } from "/imports/api/chat"; -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; export const chatModule = createModule("chat") .addMiddlewares([ @@ -242,7 +242,7 @@ export const chatModule = createModule("chat") ``` ```typescript [server/main.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { chatModule } from "./server/chat"; const server = createModule() @@ -257,7 +257,7 @@ const server = createModule() ``` ```typescript [client/main.ts] -import { createClient } from "grubba-rpc"; +import { createClient } from "meteor-rpc"; import type { Server } from "/imports/api/server"; // you must import the type const api = createClient(); @@ -275,7 +275,7 @@ This is used to build the module, it should be used at the end of the module cre ```typescript [correct.ts] // ✅ it has the build method -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; const server = createModule() .addMethod("bar", z.string(), (arg) => "bar" as const) @@ -286,7 +286,7 @@ export type Server = typeof server; ```typescript [incorrect.ts] // ❌ it is missing the build method -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; const server = createModule().addMethod( "bar", @@ -306,7 +306,7 @@ This is used to build the submodule, it should be used at the end of the submodu ::: code-group ```typescript [correct.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; export const chatModule = createModule("chat") @@ -318,7 +318,7 @@ export const chatModule = createModule("chat") ``` ```typescript [incorrect.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; export const otherSubmodule = createModule("other") @@ -338,7 +338,7 @@ export const otherSubmodule = createModule("other").addMethod( ``` ```typescript [server/main.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { chatModule } from "./server/chat"; const server = createModule() @@ -361,7 +361,7 @@ You can have something like `api.ts` that will export the client and the type of ::: code-group ```typescript [server/main.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; const server = createModule() .addMethod("bar", z.string(), (arg) => "bar" as const) @@ -391,7 +391,7 @@ It uses the [`useMutation`](https://tanstack.com/query/latest/docs/framework/rea ::: code-group ```typescript [server/main.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; const server = createModule() .addMethod("bar", z.string(), (arg) => { @@ -432,7 +432,7 @@ It uses the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/ ::: code-group ```typescript [server/main.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; const server = createModule() .addMethod("bar", z.string(), (arg) => "bar" as const) @@ -463,7 +463,7 @@ Subscriptions on the client have `useSubscription` method that can be used as a ```typescript [server/main.ts] // server/main.ts -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { ChatCollection } from "/imports/api/chat"; import { z } from "zod"; @@ -513,7 +513,7 @@ if the method has failed you can also check the error. ::: code-group ```typescript [on-method-after-creation.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; const server = createModule() @@ -537,7 +537,7 @@ export type Server = typeof server; ``` ```typescript [on-method-creation.ts] -import { createModule } from "grubba-rpc"; +import { createModule } from "meteor-rpc"; import { z } from "zod"; const server = createModule() @@ -567,3 +567,40 @@ export type Server = typeof server; ``` ::: + +## Known issues + +if you are getting a similar error like this one: + +```text + +=> Started MongoDB. +Typescript processing requested for web.browser using Typescript 5.7.2 +Creating new Typescript watcher for /app +Starting compilation in watch mode... +Compiling server/chat/model.ts +Compiling server/chat/module.ts +Compiling server/main.ts +Writing .meteor/local/plugin-cache/refapp_meteor-typescript/0.5.6/v2cache/buildfile.tsbuildinfo +Compilation finished in 0.3 seconds. 3 files were (re)compiled. +did not find /app/.meteor/local/plugin-cache/refapp_meteor-typescript/0.5.6/v2cache/out/client/main.js +did not find /app/.meteor/local/plugin-cache/refapp_meteor-typescript/0.5.6/v2cache/out/client/main.js +Nothing emitted for client/main.tsx +node:internal/crypto/hash:115 + throw new ERR_INVALID_ARG_TYPE( + ^ + +TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received null + at Hash.update (node:internal/crypto/hash:115:11) + at /Users/user/.meteor/packages/meteor-tool/.3.0.4.1tddsze.as7rh++os.osx.arm64+web.browser+web.browser.legacy+web.cordova/mt-os.osx.arm64/tools/fs/tools/fs/watch.ts:329:28 + at Array.forEach () + at /Users/user/.meteor/packages/meteor-tool/.3.0.4.1tddsze.as7rh++os.osx.arm64+web.browser+web.browser.legacy+web.cordova/mt-os.osx.arm64/tools/fs/tools/fs/watch.ts:329:8 + at JsOutputResource._get (/tools/isobuild/compiler-plugin.js:1002:19) { + code: 'ERR_INVALID_ARG_TYPE' +} + +Node.js v20.18.0 +``` + +Please check if you are using `refapp:meteor-typescript` package, if so, you can remove it and use the `typescript` package instead. +Currently, the `refapp:meteor-typescript` package is not compatible with the `meteor-rpc` package. From b61edb2f504162e704fbb9abb039e947f1883aff Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 13 Dec 2024 09:58:12 -0300 Subject: [PATCH 30/32] DEV: adjust config for vitepress --- v3-docs/docs/.vitepress/config.mts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index f57a717671..d2a7dcb97b 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -378,13 +378,22 @@ export default defineConfig({ link: "/packages/packages-listing", text: "Maintained Packages", }, + { + link: "packages/community-packages", + text: "Community Packages", + }, ], collapsed: true, }, { text: "Community Packages", link: "/community-packages/index", - items: [], + items: [ + { + text: "Meteor RPC", + link: "/community-packages/meteor-rpc", + }, + ], collapsed: true, }, { From 757eca3106e53e55451b9fe319a1f9d4ba4c656e Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 13 Dec 2024 10:43:52 -0300 Subject: [PATCH 31/32] docs: add repo link --- v3-docs/docs/community-packages/meteor-rpc.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v3-docs/docs/community-packages/meteor-rpc.md b/v3-docs/docs/community-packages/meteor-rpc.md index a830f5f9aa..45d0a89400 100644 --- a/v3-docs/docs/community-packages/meteor-rpc.md +++ b/v3-docs/docs/community-packages/meteor-rpc.md @@ -604,3 +604,5 @@ Node.js v20.18.0 Please check if you are using `refapp:meteor-typescript` package, if so, you can remove it and use the `typescript` package instead. Currently, the `refapp:meteor-typescript` package is not compatible with the `meteor-rpc` package. + +If it is still not working, please open an issue in the [repo](https://github.com/Grubba27/meteor-rpc) From 4fe24fdae934aee92a46d938dc506ab9f80366da Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 13 Dec 2024 17:12:40 -0300 Subject: [PATCH 32/32] FIX: typos in meteor-rpc.md we had a few typos in the meteor-rpc.md file, passes the whole file in Grammarly and fixed all the typos. --- v3-docs/docs/community-packages/meteor-rpc.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/v3-docs/docs/community-packages/meteor-rpc.md b/v3-docs/docs/community-packages/meteor-rpc.md index 45d0a89400..683a98be16 100644 --- a/v3-docs/docs/community-packages/meteor-rpc.md +++ b/v3-docs/docs/community-packages/meteor-rpc.md @@ -8,7 +8,7 @@ _Inspired on [zodern:relay](https://github.com/zodern/meteor-relay) and on [tRPC](https://trpc.io/)_ -This package provides functions for building E2E type-safe RPCs, focused on React front ends. +This package provides functions for building E2E type-safe RPCs focused on React front ends. ## How to download it? @@ -16,7 +16,7 @@ This package provides functions for building E2E type-safe RPCs, focused on Reac This package works only with Meteor 2.8 or higher. -If not sure about the version of Meteor you are using, you can check it by running the following command in your terminal within your project: +If you are not sure about the version of Meteor you are using, you can check it by running the following command in your terminal within your project: ```bash meteor --version @@ -30,19 +30,19 @@ meteor npm i meteor-rpc @tanstack/react-query zod ::: warning -Before continuing the installation make sure you have `react-query` all set in your project, for more info follow their [quick start guide](https://tanstack.com/query/latest/docs/framework/react/quick-start). +Before continuing the installation, make sure you have `react-query` all set in your project; for more info, follow their [quick start guide](https://tanstack.com/query/latest/docs/framework/react/quick-start). ::: ## How to use it? -There is a few concepts that are important while using this package: +There are a few concepts that are important while using this package: - This package is built on top of [`Meteor.methods`](../api/meteor.md#method-apis-methods) and [`Meteor.publish`](../api/meteor.md#publish-and-subscribe-pubsub) but with types and runtime validation, their understanding is important to use this package. -- Every method and publication use `zod` to validate the arguments, so you can be sure that the data you are receiving is the data you are expecting. +- Every method and publication uses `Zod` to validate the arguments, so you can be sure that the data you are receiving is what you expect. ::: tip -If you are accepting any type of data you can use `z.any()` as the schema or `z.void` for when there is no argument +If you are accepting any type of data, you can use `z.any()` as the schema or `z.void` when there is no argument ::: ### `createModule` @@ -53,7 +53,7 @@ This function is used to create a module that will be used to call our methods a `subModule` with a namespace: `createModule("namespace")` is used to create a submodule that will be added to the main module. -> Remember to use `build` at the end of module creation to ensure that the module is going to be created. +> Remember to use `build` at the end of module creation to ensure that the module will be created. Example: @@ -90,7 +90,7 @@ import type { Server } from "/imports/api/server"; const api = createClient(); const bar: "bar" = await api.bar("some string"); -// ?^ 'bar' +// ?^ 'bar' const newChatId = await api.chat.createChat(); // with intellisense ``` @@ -191,7 +191,7 @@ export const chatModule = createModule("chat") .addMethod("createChat", z.void(), async () => { return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] }); }) - .buildSubmodule(); // <-- this is important so that this module can be added as a submodule + .buildSubmodule(); // <-- This is important so that this module can be added as a submodule ``` ```typescript [server/chat.ts] @@ -219,9 +219,9 @@ type Middleware = (raw: unknown, parsed: unknown) => void; addMiddlewares(middlewares: Middleware[]) ``` -This is used to add middlewares to the module, it can be used to add side effects logic to the methods and publications, ideal for logging or rate limiting. +This is used to add middleware to the module; it should be used to add side effects logic to the methods and publications, which is ideal for logging or rate limiting. -The middleware ordering is last in first out. Check the example below: +The middleware ordering is last in, first out. Check the example below: ::: code-group @@ -351,7 +351,7 @@ const server = createModule() ## Using in the client -When using in the client you _have_ to use the `createModule` and `build` methods to create a module that will be used in the client +When using in the client, you _have_ to use the `createModule` and `build` methods to create a module that will be used in the client and be sure that you are exporting the type of the module _You should only create one client in your application_ @@ -382,7 +382,7 @@ await app.bar("str"); // it will return "bar" ## React focused API -Our package has a React focused API that uses `react-query` to handle the data fetching and mutations. +Our package has a React-focused API that uses `react-query` to handle the data fetching and mutations. ### `method.useMutation` @@ -500,15 +500,15 @@ export const Component = () => { ## Examples -Currently we have: +Currently, we have: - [chat-app](https://github.com/Grubba27/testing-meteor-rpc) that uses this package to create a chat-app - [askme](https://github.com/fredmaiaarantes/askme) that uses this package to create a Q&A app, you can check it live [here](https://askmeaquestion.meteorapp.com/) ## Advanced usage -you can take advantage of the hooks to add custom logic to your methods, checking the raw and parsed data, and the result of the method, -if the method has failed you can also check the error. +You can take advantage of the hooks to add custom logic to your methods, checking the raw and parsed data and the result of the method, +If the method fails, you can also check the error. ::: code-group @@ -603,6 +603,6 @@ Node.js v20.18.0 ``` Please check if you are using `refapp:meteor-typescript` package, if so, you can remove it and use the `typescript` package instead. -Currently, the `refapp:meteor-typescript` package is not compatible with the `meteor-rpc` package. +The `refapp:meteor-typescript` package is currently incompatible with the `meteor-rpc` package. If it is still not working, please open an issue in the [repo](https://github.com/Grubba27/meteor-rpc)