Merge branch 'dev-bundle-22.12.0' into dev-bundle-22.12.0

This commit is contained in:
Nacho Codoñer
2024-12-20 06:43:13 +01:00
committed by GitHub
31 changed files with 1654 additions and 317 deletions

View File

@@ -1806,21 +1806,6 @@ const setupUsersCollection = async users => {
return true;
},
updateAsync: (userId, user, fields, modifier) => {
// make sure it is our record
if (user._id !== userId) {
return false;
}
// user can only modify the 'profile' field. sets to multiple
// sub-keys (eg profile.foo and profile.bar) are merged into entry
// in the fields list.
if (fields.length !== 1 || fields[0] !== 'profile') {
return false;
}
return true;
},
fetch: ['_id'] // we only look at _id.
});
@@ -1861,4 +1846,3 @@ const generateCasePermutationsForString = string => {
}
return permutations;
}

View File

@@ -74,34 +74,38 @@ Meteor.startup(() => {
Accounts.oauth.tryLoginAfterPopupClosed = (
credentialToken,
callback,
shouldRetry = true
timeout = 1000
) => {
const credentialSecret =
OAuth._retrieveCredentialSecret(credentialToken);
let startTime = Date.now();
let calledOnce = false;
let intervalId;
const checkForCredentialSecret = (clearInterval = false) => {
const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken);
if (!calledOnce && (credentialSecret || clearInterval)) {
calledOnce = true;
Meteor.clearInterval(intervalId);
Accounts.callLoginMethod({
methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
userCallback: callback ? err => callback(convertError(err)) : () => {},
});
} else if (clearInterval) {
Meteor.clearInterval(intervalId);
}
};
// Check immediately
checkForCredentialSecret();
// Then check on an interval
// In some case the function OAuth._retrieveCredentialSecret() can return null, because the local storage might not
// be ready. So we retry after a timeout.
if (!credentialSecret) {
if (!shouldRetry) {
return;
intervalId = Meteor.setInterval(() => {
if (Date.now() - startTime > timeout) {
checkForCredentialSecret(true);
} else {
checkForCredentialSecret();
}
Meteor.setTimeout(
() =>
Accounts.oauth.tryLoginAfterPopupClosed(
credentialToken,
callback,
false
),
500
);
return;
}
// continue with the rest of the function
Accounts.callLoginMethod({
methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
userCallback: callback && (err => callback(convertError(err))),
});
}, 250);
};
Accounts.oauth.credentialRequestCompleteHandler = callback =>
@@ -112,4 +116,3 @@ Accounts.oauth.credentialRequestCompleteHandler = callback =>
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback);
}
}

View File

@@ -49,7 +49,7 @@ const CollectionPrototype = AllowDeny.CollectionPrototype;
* @memberOf Mongo.Collection
* @instance
* @param {Object} options
* @param {Function} options.insertAsync,updateAsync,removeAsync Functions that look at a proposed modification to the database and return true if it should be allowed.
* @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be allowed.
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
*/
@@ -64,7 +64,7 @@ CollectionPrototype.allow = function(options) {
* @memberOf Mongo.Collection
* @instance
* @param {Object} options
* @param {Function} options.insertAsync,updateAsync,removeAsync Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
* @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
*/
@@ -554,7 +554,7 @@ function addValidator(collection, allowOrDeny, options) {
const isAsyncKey = key.includes('Async');
if (isAsyncKey) {
const syncKey = key.replace('Async', '');
console.warn(allowOrDeny + `: The "${key}" key is deprecated. Use "${syncKey}" instead.`);
Meteor.deprecate(allowOrDeny + `: The "${key}" key is deprecated. Use "${syncKey}" instead.`);
}
});

View File

@@ -1,6 +1,6 @@
Package.describe({
name: 'allow-deny',
version: '2.0.0',
version: '2.1.0-beta311.1',
// Brief, one-line summary of the package.
summary: 'Implements functionality for allow/deny and client-side db operations',
// URL to the Git repository containing the source code for this package.

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Meteor's latency-compensated distributed data client",
version: "3.0.3",
version: "3.1.0-beta311.1",
documentation: null,
});

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Meteor's latency-compensated distributed data server",
version: "3.0.3",
version: "3.1.0-beta311.1",
documentation: null,
});

View File

@@ -26,11 +26,12 @@ Facebook.handleAuthFromAccessToken = async (accessToken, expiresAt) => {
};
};
Accounts.registerLoginHandler(request => {
Accounts.registerLoginHandler(async request => {
if (request.facebookSignIn !== true) {
return;
}
const facebookData = Facebook.handleAuthFromAccessToken(request.accessToken, (+new Date) + (1000 * request.expirationTime));
const facebookData = await Facebook.handleAuthFromAccessToken(request.accessToken, (+new Date) + (1000 * request.expirationTime));
if (!facebookData) return;
return Accounts.updateOrCreateUserFromExternalService('facebook', facebookData.serviceData, facebookData.options);
});
@@ -96,7 +97,6 @@ const getTokenResponse = async (query) => {
.then((res) => res.json())
.then(data => {
const fbAccessToken = data.access_token;
console.log("-> fbAccessToken", fbAccessToken);
const fbExpires = data.expires_in;
if (!fbAccessToken) {
throw new Error("Failed to complete OAuth handshake with facebook " +

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "The Meteor command-line tool",
version: "3.1.0",
version: "3.1.1-beta.1",
});
Package.includeTool();

View File

@@ -0,0 +1,37 @@
function cleanStackTrace(stackTrace) {
if (!stackTrace || typeof stackTrace !== 'string') return [];
var lines = stackTrace.split('\n');
var trace = [];
try {
for (var i = 0; i < lines.length; i++) {
var _line = lines[i].trim();
if (_line.indexOf('Meteor.deprecate') !== -1) continue;
if (_line.indexOf('packages/') !== -1) {
trace.push(_line);
} else if (_line && _line.indexOf('/') !== -1) {
// Stop processing if a valid path that does not start with 'packages/**' is found
trace.push(_line);
break;
}
}
} catch (e) {
console.error('Error cleaning stack trace: ', e);
}
return trace.join('\n');
}
Meteor.deprecate = function () {
if (!Meteor.isDevelopment) {
return;
}
if (typeof console !== 'undefined' && typeof console.warn !== 'undefined') {
var stackStrace = cleanStackTrace(new Error().stack || '');
var messages = Array.prototype.slice.call(arguments); // Convert arguments to array
if (stackStrace.length > 0) {
messages.push('\n\n', 'Trace:', '\n', stackStrace);
}
console.warn.apply(console, ['[DEPRECATION]'].concat(messages));
}
};

View File

@@ -2,7 +2,7 @@
Package.describe({
summary: "Core Meteor environment",
version: '2.0.2',
version: '2.1.0-beta311.1',
});
Package.registerBuildPlugin({
@@ -41,6 +41,7 @@ Package.onUse(function (api) {
api.addFiles('startup_client.js', ['client']);
api.addFiles('startup_server.js', ['server']);
api.addFiles('debug.js', ['client', 'server']);
api.addFiles('deprecate.js', ['client', 'server']);
api.addFiles('string_utils.js', ['client', 'server']);
api.addFiles('test_environment.js', ['client', 'server']);

View File

@@ -96,7 +96,7 @@ export namespace Mongo {
interface Collection<T extends NpmModuleMongodb.Document, U = T> {
allow<Fn extends Transform<T> = undefined>(options: {
insert?:
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => boolean)
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => Promise<boolean>|boolean)
| undefined;
update?:
| ((
@@ -104,10 +104,10 @@ export namespace Mongo {
doc: DispatchTransform<Fn, T, U>,
fieldNames: string[],
modifier: any
) => boolean)
) => Promise<boolean>|boolean)
| undefined;
remove?:
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => boolean)
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => Promise<boolean>|boolean)
| undefined;
fetch?: string[] | undefined;
transform?: Fn | undefined;
@@ -131,7 +131,7 @@ export namespace Mongo {
): Promise<void>;
deny<Fn extends Transform<T> = undefined>(options: {
insert?:
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => boolean)
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => Promise<boolean>|boolean)
| undefined;
update?:
| ((
@@ -139,10 +139,10 @@ export namespace Mongo {
doc: DispatchTransform<Fn, T, U>,
fieldNames: string[],
modifier: any
) => boolean)
) => Promise<boolean>|boolean)
| undefined;
remove?:
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => boolean)
| ((userId: string, doc: DispatchTransform<Fn, T, U>) => Promise<boolean>|boolean)
| undefined;
fetch?: string[] | undefined;
transform?: Fn | undefined;

View File

@@ -1,7 +1,9 @@
import clone from 'lodash.clone'
/** @type {import('mongodb')} */
export const MongoDB = NpmModuleMongodb;
export const MongoDB = Object.assign(NpmModuleMongodb, {
ObjectID: NpmModuleMongodb.ObjectId,
});
// The write methods block until the database has confirmed the write (it may
// not be replicated or stable on disk, but one server has confirmed it) if no

View File

@@ -18,7 +18,17 @@ MongoInternals.NpmModules = {
// MongoInternals.NpmModules.mongodb.module. It was never documented, but
// people do use it.
// XXX COMPAT WITH 1.0.3.2
MongoInternals.NpmModule = MongoDB;
MongoInternals.NpmModule = new Proxy(MongoDB, {
get(target, propertyKey, receiver) {
if (propertyKey === 'ObjectID') {
Meteor.deprecate(
`Accessing 'MongoInternals.NpmModule.ObjectID' directly is deprecated. ` +
`Use 'MongoInternals.NpmModule.ObjectId' instead.`
);
}
return Reflect.get(target, propertyKey, receiver);
},
});
MongoInternals.OplogHandle = OplogHandle;

View File

@@ -9,7 +9,7 @@
Package.describe({
summary: "Adaptor for using MongoDB and Minimongo over DDP",
version: "2.0.3",
version: "2.1.0-beta311.1",
});
Npm.depends({

View File

@@ -3967,6 +3967,11 @@ Meteor.isServer &&
typeof MongoInternals.NpmModules.mongodb.module.ObjectId,
'function'
);
test.equal(
MongoInternals.NpmModule.ObjectID,
MongoInternals.NpmModule.ObjectId,
'MongoInternals.ObjectID should be an alias for MongoInternals.ObjectId'
);
var c = new Mongo.Collection(Random.id());
var rawCollection = c.rawCollection();

View File

@@ -3,7 +3,7 @@
Package.describe({
summary: "Wrapper around the mongo npm package",
version: "6.10.1",
version: "6.10.2-beta311.1",
documentation: null,
});

View File

@@ -34,9 +34,17 @@ OAuth.showPopup = (url, callback, dimensions) => {
throw new Error("No hash fragment in OAuth popup?");
}
const credentials = JSON.parse(decodeURIComponent(hashFragment));
OAuth._handleCredentialSecret(credentials.credentialToken,
credentials.credentialSecret);
try {
const credentials = JSON.parse(decodeURIComponent(hashFragment));
OAuth._handleCredentialSecret(credentials.credentialToken,
credentials.credentialSecret);
} catch (error) {
if (error instanceof SyntaxError) {
// Ignore or a default value if parsing fails
} else {
throw error; // Re-throw if it's not a syntax error
}
}
oauthFinished = true;
@@ -56,6 +64,12 @@ OAuth.showPopup = (url, callback, dimensions) => {
};
const onExit = () => {
// Force callback to throw cancel error when exit
// and oauth process didn't start
if (!oauthFinished) {
callback();
}
popup.removeEventListener('loadstop', pageLoaded);
popup.removeEventListener('loaderror', fail);
popup.removeEventListener('exit', onExit);

View File

@@ -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<void>
): Promise<void>;
/**
* 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<string>
function createRole(
roleName: string,
options?: { unlessExists: boolean }
): string;
function createRoleAsync(
roleName: string,
options?: { unlessExists: boolean }
): Promise<string>;
/**
* 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<void>
function deleteRole(roleName: string): void;
function deleteRoleAsync(roleName: string): Promise<void>;
/**
* 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<void>
function renameRole(oldName: string, newName: string): void;
function renameRoleAsync(oldName: string, newName: string): Promise<void>;
/**
* 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<void>
function addRolesToParent(
rolesNames: string | string[],
parentName: string
): void;
function addRolesToParentAsync(
rolesNames: string | string[],
parentName: string
): Promise<void>;
/**
* 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<void>
function removeRolesFromParent(
rolesNames: string | string[],
parentName: string
): void;
function removeRolesFromParentAsync(
rolesNames: string | string[],
parentName: string
): Promise<void>;
/**
* 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<Role>
function getAllRoles(queryOptions?: QueryOptions): Mongo.Cursor<Role>;
/**
* 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<string[]>
function getGroupsForUser(
user: string | Meteor.User,
role?: string
): string[];
function getGroupsForUserAsync(
user: string | Meteor.User,
role?: string
): Promise<string[]>;
/**
* 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<string[]>
function getScopesForUser(
user: string | Meteor.User,
roles?: string | string[]
): string[];
function getScopesForUserAsync(
user: string | Meteor.User,
roles?: string | string[]
): Promise<string[]>;
/**
* 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<void>
function renameScope(oldName: string, newName: string): void;
function renameScopeAsync(oldName: string, newName: string): Promise<void>;
/**
* 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<void>
function removeScope(name: String): void;
function removeScopeAsync(name: String): Promise<void>;
/**
* 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<boolean>
function isParentOf(parentRoleName: string, childRoleName: string): boolean;
function isParentOfAsync(
parentRoleName: string,
childRoleName: string
): Promise<boolean>;
/**
* 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<string[]>
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<string[]>;
/**
* 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<RoleAssignment>
function getUserAssignmentsForRole(
roles: string | string[],
options?:
| string
| {
scope?: string;
anyScope?: boolean;
queryOptions?: QueryOptions;
}
): Mongo.Cursor<RoleAssignment>;
/**
* 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<Meteor.User>
): Mongo.Cursor<Meteor.User>;
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<Mongo.Cursor<Meteor.User>>
): Promise<Mongo.Cursor<Meteor.User>>;
/**
* 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<void>
): Promise<void>;
/**
* 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<void>
options?:
| string
| { scope?: string; anyScope?: boolean; ifExists?: boolean }
): Promise<void>;
/**
* 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<boolean>
): Promise<boolean>;
// 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<Roles.Role>
declare type RoleAssignmentsCollection = Mongo.Collection<Roles.RoleAssignment>
export declare type RolesCollection = Mongo.Collection<Roles.Role>;
export declare type RoleAssignmentsCollection =
Mongo.Collection<Roles.RoleAssignment>;
// Additions to the Meteor object
declare module 'meteor/meteor' {
namespace Meteor {
const roles: Mongo.Collection<Roles.Role>
const roleAssignment: Mongo.Collection<Roles.RoleAssignment>
declare module "meteor/meteor" {
export namespace Meteor {
const roles: Mongo.Collection<Roles.Role>;
const roleAssignment: Mongo.Collection<Roles.RoleAssignment>;
}
}

View File

@@ -0,0 +1,3 @@
{
"typesEntry": "definitions.d.ts"
}

View File

@@ -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"]);
@@ -25,17 +23,15 @@ 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", "server");
api.addAssets("package-types.json", "server");
});
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");

View File

@@ -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<String>} ID of the new role or null.
* @static
* - `unlessExists`: if `true`, exception will not be thrown in the role already exists
* @return {Promise<String>} 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<Boolean>} `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>} 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>} 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<Boolean>} 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>} 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)

View File

@@ -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) => {
);
};
const 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;

View File

@@ -1,6 +1,6 @@
Package.describe({
name: "socket-stream-client",
version: '0.5.3',
version: '0.6.0-beta311.1',
summary: "Provides the ClientStream abstraction used by ddp-client",
documentation: "README.md"
});

View File

@@ -1,6 +1,6 @@
{
"track": "METEOR",
"version": "3.1-rc.0",
"version": "3.1.1-beta.1",
"recommended": false,
"official": false,
"description": "Meteor experimental release"

View File

@@ -10,7 +10,7 @@ var packageJson = {
dependencies: {
// Explicit dependency because we are replacing it with a bundled version
// and we want to make sure there are no dependencies on a higher version
npm: "10.9.0",
npm: "10.9.2",
"node-gyp": "10.2.0",
"@mapbox/node-pre-gyp": "1.0.11",
typescript: "5.6.3",

View File

@@ -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",
@@ -374,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,
},
{

View File

@@ -23,3 +23,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), Meteor Methods Evolved with type checking and runtime validation

View File

@@ -0,0 +1,608 @@
# 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?
::: warning
This package works only with Meteor 2.8 or higher.
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
```
:::
```bash
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).
:::
## How to use it?
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 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` 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 will be created.
Example:
::: code-group
```typescript [server/main.ts]
import { createModule } from "meteor-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 "meteor-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 "meteor-rpc";
// you must import the type of the server
import type { Server } from "/imports/api/server";
const api = createClient<Server>();
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<ZodSchema>) => T,
config?: Config<ZodTypeInput<ZodSchema>, 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 "meteor-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<ZodSchema>) => Cursor<Result, Result>
)
```
This is the equivalent of `Meteor.publish` but with types and runtime validation.
::: code-group
```typescript [server/with-meteor-rpc.ts]
import { createModule } from "meteor-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 "meteor-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 "meteor-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 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:
::: code-group
```typescript [server/chat.ts]
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "meteor-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 "meteor-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 "meteor-rpc";
import type { Server } from "/imports/api/server"; // you must import the type
const api = createClient<Server>();
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 "meteor-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 "meteor-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 "meteor-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 "meteor-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 "meteor-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 "meteor-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<Server>();
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 "meteor-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<Server>();
export const Component = () => {
const { mutate, isLoading, isError, error, data } = app.bar.useMutation();
return (
<button
onClick={() => {
mutation.mutate("str");
}}
>
Click me
</button>
);
};
```
:::
### `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 "meteor-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<Server>();
export const Component = () => {
const { data } = app.bar.useQuery("str"); // this will trigger suspense
return <div>{data}</div>;
};
```
:::
### `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 "meteor-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<Server>();
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 (
<div>
{rooms.map((room) => (
<div key={room._id}>{room.name}</div>
))}
</div>
);
};
```
:::
## 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 fails, you can also check the error.
::: code-group
```typescript [on-method-after-creation.ts]
import { createModule } from "meteor-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 "meteor-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;
```
:::
## 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 (<anonymous>)
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.
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)

View File

@@ -0,0 +1,52 @@
## v3.1.1, /date/
### Highlights
- Real-time Performance Boost: Refactored AsynchronousQueue for parallel processing and optimized MongoDB observers for faster initial document handling.
- Allow/Deny Rules Update: Deprecated async rules and updated documentation and types.
- Mongo Driver Downgrade: Reverted to a stable version to prevent issues in Meteor.
- [...]
#### Breaking Changes
N/A
#### Internal API changes
N/A
#### Migration Steps
Please run the following command to update your project:
```bash
meteor update --release 3.1.1-beta.0
```
#### Bumped Meteor Packages
- allow-deny@2.1.0
- ddp-client@3.1.0
- ddp-server@3.1.0
- meteor-tool@3.1.1
- meteor@2.1.0
- mongo@2.1.0
- npm-mongo@6.10.2
- socket-stream-client@0.6.0
- [...]
#### Bumped NPM Packages
N/A
#### Special thanks to
✨✨✨
- [@leonardoventurini](https://github.com/leonardoventurini)
- [@nachocodoner](https://github.com/nachocodoner)
- [@Grubba27](https://github.com/Grubba27)
✨✨✨

View File

@@ -0,0 +1,480 @@
# Roles
Authorization package for Meteor - compatible with built-in accounts package.
> Available since Meteor 3.1.0 (previously alanning:roles)
## 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
<ApiBox name="Roles.createRoleAsync" hasCustomExample/>
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
<ApiBox name="Roles.addRolesToParentAsync" hasCustomExample />
Example:
```js
// Make 'editor' a child role of 'admin'
await Roles.addRolesToParentAsync("editor", "admin");
// Add multiple child roles
await Roles.addRolesToParentAsync(["editor", "moderator"], "admin");
```
<ApiBox name="Roles.removeRolesFromParentAsync" hasCustomExample />
Example:
```js
// Remove 'editor' as child role of 'admin'
await Roles.removeRolesFromParentAsync("editor", "admin");
```
<ApiBox name="Roles.deleteRoleAsync" hasCustomExample />
Example:
```js
// Delete role and all its assignments
await Roles.deleteRoleAsync("temp-role");
```
<ApiBox name="Roles.renameRoleAsync" hasCustomExample />
Example:
```js
// Rename an existing role
await Roles.renameRoleAsync("editor", "content-editor");
```
### Assigning Roles
<ApiBox name="Roles.addUsersToRolesAsync" hasCustomExample />
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"]);
```
<ApiBox name="Roles.setUserRolesAsync" hasCustomExample />
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");
```
<ApiBox name="Roles.removeUsersFromRolesAsync" hasCustomExample />
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"]);
```
<ApiBox name="Roles.renameScopeAsync" hasCustomExample />
Example:
```js
// Rename a scope
await Roles.renameScopeAsync("department-1", "marketing");
```
<ApiBox name="Roles.removeScopeAsync" hasCustomExample />
Example:
```js
// Remove a scope and all its role assignments
await Roles.removeScopeAsync("old-department");
```
<ApiBox name="Roles.getAllRoles" hasCustomExample />
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 },
});
```
<ApiBox name="Roles.getUsersInRoleAsync" hasCustomExample />
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
<ApiBox name="Roles.userIsInRoleAsync" hasCustomExample />
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,
});
```
<ApiBox name="Roles.getRolesForUserAsync" hasCustomExample />
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,
});
```
<ApiBox name="Roles.isParentOfAsync" hasCustomExample />
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");
```
<ApiBox name="Roles.getScopesForUserAsync" hasCustomExample />
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();
});
```
## 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:
```handlebars
{{#if isInRole "admin"}}
<div class="admin-panel">
<!-- Admin only content -->
</div>
{{/if}}
{{#if isInRole "editor,writer" "blog"}}
<div class="editor-tools">
<!-- Blog editor tools -->
</div>
{{/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
```
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.
## 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);
},
});
```

View File

@@ -1,7 +1,7 @@
---
meteor_version: 3.1.0
node_version: 22.12.0
npm_version: 10.9.0
npm_version: 10.9.2
---
# Meteor 3.0 Migration Guide