@7365admin1/core 3.134.0 → 3.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.135.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ab7ea6f: Organisation roles used per site: Members -> Site access also offers the organisation's service roles (no site); every row it writes is stamped `createdBy`, which `siteInvitedPins` reads as "placed at this site", so such a row reaches that site only. Assign Role stamps the same when it gives an organisation role to a site-pinned membership. `GET /roles?scope=organisation` lists only roles with no site (and never self-heals).
8
+
3
9
  ## 3.134.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -327,7 +327,7 @@ declare function useRoleRepo(): {
327
327
  getRoleByUserId: (value: string | ObjectId) => Promise<TRole | null>;
328
328
  getRoleById: (_id: string | ObjectId) => Promise<TRole | null>;
329
329
  getRoleByName: (name: string, type?: string) => Promise<TRole | null>;
330
- getRoles: ({ search, page, limit, sort, type, org, site, }: {
330
+ getRoles: ({ search, page, limit, sort, type, org, site, scope, }: {
331
331
  search?: string | undefined;
332
332
  page?: number | undefined;
333
333
  limit?: number | undefined;
@@ -335,6 +335,8 @@ declare function useRoleRepo(): {
335
335
  type?: string | undefined;
336
336
  org: string | ObjectId;
337
337
  site?: string | ObjectId | undefined;
338
+ /** "organisation": roles with no site only (ignored when `site` is given) */
339
+ scope?: string | undefined;
338
340
  }) => Promise<{}>;
339
341
  getActiveByTypeOrg: (type: string, org: string | ObjectId) => Promise<TRole[]>;
340
342
  updateRole: (_id: string | ObjectId, value: TMiniRole, session?: ClientSession) => Promise<ObjectId>;
@@ -505,7 +507,7 @@ declare function useMemberRepo(): {
505
507
  updateMemberStatus: (_id: string | ObjectId, value: TMemberUpdateStatus) => Promise<number>;
506
508
  countByOrg: (org: string | ObjectId) => Promise<number | TMember>;
507
509
  countUserMembershipById: (user: string | ObjectId) => Promise<number | TMember>;
508
- updateRoleById: (_id: string | ObjectId, role: string | ObjectId) => Promise<number>;
510
+ updateRoleById: (_id: string | ObjectId, role: string | ObjectId, createdBy?: string | ObjectId) => Promise<number>;
509
511
  reassignRoles: (reassignments: Array<{
510
512
  memberId: string;
511
513
  roleId: string;
@@ -3235,6 +3237,13 @@ declare function pinKey(org: unknown, site: unknown): string;
3235
3237
  * row to test (the usual case). Staging, 2026-09-28: 4 of 168 pinned
3236
3238
  * org-level-role memberships match, so the stray-`siteId` rule is untouched
3237
3239
  * for the rest.
3240
+ *
3241
+ * Also confined (owner request 2026-09-30, organisation roles used per site): a
3242
+ * pinned row somebody ELSE wrote - `createdBy` set and not the member. Only
3243
+ * Members -> Site access writes that (`member-site-access.service`); the one
3244
+ * other writer, the org owner's own row at onboarding, has `createdBy` = the
3245
+ * owner and no site. So "the owner placed this person at this site" narrows
3246
+ * exactly like "this person was invited to this site".
3238
3247
  */
3239
3248
  declare function siteInvitedPins(db: Db, userId: string, memberships: Array<Record<string, any>>): Promise<Set<string>>;
3240
3249
  /**
package/dist/index.js CHANGED
@@ -5440,7 +5440,7 @@ function useMemberRepo() {
5440
5440
  throw error;
5441
5441
  }
5442
5442
  }
5443
- async function updateRoleById(_id, role) {
5443
+ async function updateRoleById(_id, role, createdBy) {
5444
5444
  try {
5445
5445
  _id = new import_mongodb14.ObjectId(_id);
5446
5446
  } catch (error) {
@@ -5456,6 +5456,9 @@ function useMemberRepo() {
5456
5456
  role,
5457
5457
  updatedAt: /* @__PURE__ */ new Date()
5458
5458
  };
5459
+ if (createdBy && import_mongodb14.ObjectId.isValid(String(createdBy))) {
5460
+ updateValue.createdBy = new import_mongodb14.ObjectId(String(createdBy));
5461
+ }
5459
5462
  const res = await collection.updateOne({ _id }, { $set: updateValue });
5460
5463
  if (res.modifiedCount === 0) {
5461
5464
  throw new import_node_server_utils15.InternalServerError("Unable to update feedback status.");
@@ -9928,11 +9931,27 @@ async function siteInvitedPins(db2, userId, memberships) {
9928
9931
  const keys = /* @__PURE__ */ new Set();
9929
9932
  if (!pinned.length || !import_mongodb26.ObjectId.isValid(userId))
9930
9933
  return keys;
9934
+ const both3 = (ids2) => [...new Set(ids2.map(String))].flatMap((v) => [v, new import_mongodb26.ObjectId(v)]);
9935
+ const wanted = new Set(pinned.map((m) => pinKey(m.org, m.siteId)));
9936
+ const placed = await db2.collection("members").find(
9937
+ {
9938
+ user: new import_mongodb26.ObjectId(userId),
9939
+ status: { $ne: "deleted" },
9940
+ org: { $in: both3(pinned.map((m) => m.org)) },
9941
+ siteId: { $in: both3(pinned.map((m) => m.siteId)) },
9942
+ createdBy: { $type: "objectId", $ne: new import_mongodb26.ObjectId(userId) }
9943
+ },
9944
+ { projection: { org: 1, siteId: 1 } }
9945
+ ).toArray();
9946
+ for (const row of placed) {
9947
+ const key = pinKey(row.org, row.siteId);
9948
+ if (wanted.has(key))
9949
+ keys.add(key);
9950
+ }
9931
9951
  const user = await db2.collection("users").findOne({ _id: new import_mongodb26.ObjectId(userId) }, { projection: { email: 1 } });
9932
9952
  const email = String(user?.email ?? "").trim();
9933
9953
  if (!email)
9934
9954
  return keys;
9935
- const both3 = (ids2) => [...new Set(ids2.map(String))].flatMap((v) => [v, new import_mongodb26.ObjectId(v)]);
9936
9955
  const invites = await db2.collection("verifications").find(
9937
9956
  {
9938
9957
  email,
@@ -9948,7 +9967,6 @@ async function siteInvitedPins(db2, userId, memberships) {
9948
9967
  collation: { locale: "en", strength: 2 }
9949
9968
  }
9950
9969
  ).toArray();
9951
- const wanted = new Set(pinned.map((m) => pinKey(m.org, m.siteId)));
9952
9970
  for (const invite of invites) {
9953
9971
  const key = pinKey(invite.metadata?.org, invite.metadata?.siteId);
9954
9972
  if (wanted.has(key))
@@ -25792,7 +25810,8 @@ function useRoleRepo() {
25792
25810
  sort = {},
25793
25811
  type = "",
25794
25812
  org = "",
25795
- site = ""
25813
+ site = "",
25814
+ scope = ""
25796
25815
  }) {
25797
25816
  page2 = page2 > 0 ? page2 - 1 : 0;
25798
25817
  if (org && typeof org === "string" && org.length === 24) {
@@ -25814,6 +25833,9 @@ function useRoleRepo() {
25814
25833
  if (site) {
25815
25834
  query2.$or = [{ site }, { site: "" }, { site: null }];
25816
25835
  cacheOptions.site = site.toString();
25836
+ } else if (scope === "organisation") {
25837
+ query2.site = { $in: ["", null] };
25838
+ cacheOptions.scope = scope;
25817
25839
  }
25818
25840
  sort = Object.keys(sort).length > 0 ? sort : { _id: -1 };
25819
25841
  cacheOptions.sort = JSON.stringify(sort);
@@ -28250,7 +28272,9 @@ function useRoleController() {
28250
28272
  limit: import_joi28.default.number().integer().min(1).max(100).allow("", null).default(10),
28251
28273
  type: import_joi28.default.string().optional().allow("", null),
28252
28274
  org: import_joi28.default.string().hex().optional().allow("", null),
28253
- site: import_joi28.default.string().hex().optional().allow("", null)
28275
+ site: import_joi28.default.string().hex().optional().allow("", null),
28276
+ // "organisation": only the roles with no site (Organization app, service roles made once)
28277
+ scope: import_joi28.default.string().valid("organisation").optional().allow("", null)
28254
28278
  });
28255
28279
  const query2 = { ...req.query };
28256
28280
  const { error } = validation.validate(query2);
@@ -28265,18 +28289,19 @@ function useRoleController() {
28265
28289
  const type = req.query.type ?? "";
28266
28290
  const org = req.query.org ?? "";
28267
28291
  const site = req.query.site ?? "";
28292
+ const scope = req.query.scope ?? "";
28268
28293
  try {
28269
28294
  if (org) {
28270
28295
  await requireOrgReach(req, org);
28271
28296
  }
28272
- let data = await _getRoles({ search, page: page2, limit, type, org, site });
28273
- const listIsWholeOrg = org && !search && !site && page2 === 1;
28297
+ let data = await _getRoles({ search, page: page2, limit, type, org, site, scope });
28298
+ const listIsWholeOrg = org && !search && !site && !scope && page2 === 1;
28274
28299
  const listed = data?.items;
28275
28300
  if (listIsWholeOrg && listed?.length === 0) {
28276
28301
  const seeded = await healDefaultRoles(org).catch(() => 0);
28277
28302
  if (seeded > 0) {
28278
28303
  try {
28279
- data = await _getRoles({ search, page: page2, limit, type, org, site });
28304
+ data = await _getRoles({ search, page: page2, limit, type, org, site, scope });
28280
28305
  } catch (error2) {
28281
28306
  import_node_server_utils71.logger.log({ level: "error", message: error2.message });
28282
28307
  }
@@ -28724,7 +28749,8 @@ function useMemberService() {
28724
28749
  }
28725
28750
  }
28726
28751
  }
28727
- await _updateRoleById(id, role);
28752
+ const placedBy = member.siteId && !newRole?.site && callerId5 && import_mongodb60.ObjectId.isValid(callerId5) && String(member.user ?? "") !== callerId5 ? callerId5 : void 0;
28753
+ await _updateRoleById(id, role, placedBy);
28728
28754
  return { message: "Member role updated successfully." };
28729
28755
  }
28730
28756
  async function updateSiteById(id, siteId, siteName) {
@@ -28772,6 +28798,7 @@ function useMemberService() {
28772
28798
  var import_node_server_utils73 = require("@7365admin1/node-server-utils");
28773
28799
  var import_mongodb61 = require("mongodb");
28774
28800
  var EXCLUDED_TYPES = ["resident", PLATFORM_STAFF_MEMBER_TYPE];
28801
+ var ORG_ROLE_EXCLUDED = [...EXCLUDED_TYPES, "organization"];
28775
28802
  var both = (id) => [id, new import_mongodb61.ObjectId(id)];
28776
28803
  function useMemberSiteAccessService() {
28777
28804
  const { add, updateMemberStatus, updateRoleById } = useMemberRepo();
@@ -28811,10 +28838,13 @@ function useMemberSiteAccessService() {
28811
28838
  const roles = await db2().collection("roles").find({
28812
28839
  $or: [
28813
28840
  { _id: { $in: roleIds.map((r) => new import_mongodb61.ObjectId(r)) } },
28814
- { org: { $in: both(org) }, site: { $in: [...sites.keys()].flatMap(both) }, status: "active" }
28841
+ { org: { $in: both(org) }, site: { $in: [...sites.keys()].flatMap(both) }, status: "active" },
28842
+ // organisation-level service roles (no site; never the org-console type)
28843
+ { org: { $in: both(org) }, site: { $in: ["", null] }, status: "active", type: { $nin: ORG_ROLE_EXCLUDED } }
28815
28844
  ]
28816
28845
  }).project({ name: 1, type: 1, site: 1, org: 1, status: 1 }).toArray();
28817
28846
  const byId = new Map(roles.map((r) => [String(r._id), r]));
28847
+ const orgRoles = roles.filter((r) => !String(r.site ?? "") && String(r.org ?? "") === org && r.status === "active" && !ORG_ROLE_EXCLUDED.includes(r.type)).map((r) => ({ _id: String(r._id), name: r.name ?? "", type: r.type ?? "", scope: "organisation" })).sort((a, b) => a.name.localeCompare(b.name));
28818
28848
  const view = (m) => ({
28819
28849
  _id: String(m._id),
28820
28850
  role: String(m.role ?? ""),
@@ -28828,7 +28858,7 @@ function useMemberSiteAccessService() {
28828
28858
  ...s,
28829
28859
  membership: here[0] ?? null,
28830
28860
  memberships: here,
28831
- roles: roles.filter((r) => String(r.site ?? "") === s.site && String(r.org ?? "") === org && r.status === "active" && !EXCLUDED_TYPES.includes(r.type)).map((r) => ({ _id: String(r._id), name: r.name ?? "", type: r.type ?? "" }))
28861
+ roles: roles.filter((r) => String(r.site ?? "") === s.site && String(r.org ?? "") === org && r.status === "active" && !EXCLUDED_TYPES.includes(r.type)).map((r) => ({ _id: String(r._id), name: r.name ?? "", type: r.type ?? "", scope: "site" })).concat(orgRoles)
28832
28862
  };
28833
28863
  }).sort((a, b) => a.name.localeCompare(b.name));
28834
28864
  const orgWide = members.filter((m) => !String(m.siteId ?? "")).map(view);
@@ -28854,7 +28884,7 @@ function useMemberSiteAccessService() {
28854
28884
  throw new import_node_server_utils73.BadRequestError("A site is listed twice.");
28855
28885
  seen2.add(choice.site);
28856
28886
  if (!row.roles.some((r) => r._id === choice.role)) {
28857
- throw new import_node_server_utils73.BadRequestError(`Choose a role created for ${row.name || "that site"}.`);
28887
+ throw new import_node_server_utils73.BadRequestError(`Choose a role created for ${row.name || "that site"}, or one of the organisation's service roles.`);
28858
28888
  }
28859
28889
  }
28860
28890
  const userDoc = await db2().collection("users").findOne({ _id: new import_mongodb61.ObjectId(user) }, { projection: { email: 1, name: 1 } });
@@ -28868,10 +28898,11 @@ function useMemberSiteAccessService() {
28868
28898
  const choice = sites.find((c) => c.site === row.site);
28869
28899
  const role = choice ? row.roles.find((r) => r._id === choice.role) : null;
28870
28900
  const keep = role ? row.memberships.find((m) => m.type === role.type) : void 0;
28871
- if (role && keep && keep.role !== role._id) {
28901
+ if (role && keep && (keep.role !== role._id || role.scope === "organisation")) {
28872
28902
  reroles.push(async () => {
28873
- await updateRoleById(keep._id, role._id);
28874
- applied.changed.push(row.site);
28903
+ await updateRoleById(keep._id, role._id, actor);
28904
+ if (keep.role !== role._id)
28905
+ applied.changed.push(row.site);
28875
28906
  });
28876
28907
  } else if (role && !keep) {
28877
28908
  adds.push(async () => {
@@ -28880,7 +28911,7 @@ function useMemberSiteAccessService() {
28880
28911
  { projection: { _id: 1 } }
28881
28912
  );
28882
28913
  if (old) {
28883
- await updateRoleById(old._id, role._id);
28914
+ await updateRoleById(old._id, role._id, actor);
28884
28915
  await updateMemberStatus(old._id, { status: suspended ? "suspended" : "active" });
28885
28916
  } else {
28886
28917
  await add({
@@ -28894,6 +28925,8 @@ function useMemberSiteAccessService() {
28894
28925
  siteId: row.site,
28895
28926
  siteName: row.name,
28896
28927
  dateInvited: (/* @__PURE__ */ new Date()).toISOString(),
28928
+ // "placed at this site by the editor": confines an organisation role to it (siteInvitedPins)
28929
+ createdBy: actor,
28897
28930
  ...suspended ? { status: "suspended" } : {}
28898
28931
  });
28899
28932
  }