placementt-core 1.400.1249 → 1.400.1250

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.
@@ -65,6 +65,11 @@ export type GlobalSearchIndexDefinition = {
65
65
  filters: (user: UserData, institute?: InstituteData) => string;
66
66
  /** Turns one raw hit into a row. */
67
67
  toHit: (hit: any) => GlobalSearchHit;
68
+ /**
69
+ * Drops hits the filter couldn't, for records that don't carry what the user's
70
+ * access depends on (a placement holds its student's uid, not their details).
71
+ */
72
+ keepHits?: (hits: any[], user: UserData) => Promise<any[]>;
68
73
  } | {
69
74
  /**
70
75
  * Searched in memory, from records the caller already holds, instead of
@@ -72,6 +77,44 @@ export type GlobalSearchIndexDefinition = {
72
77
  */
73
78
  local: (params: GlobalSearchParams) => GlobalSearchHit[];
74
79
  });
80
+ export type SearchStudentScope = {
81
+ kind: "all";
82
+ } | {
83
+ kind: "none";
84
+ } | {
85
+ kind: "some";
86
+ field: string;
87
+ values: string[];
88
+ };
89
+ /**
90
+ * The students a staff member may find, by the Students list's rule: admins and
91
+ * "all" see everyone, "some" sees students whose `details[studentFilter]` is one of
92
+ * `studentFilterValues`, anyone else sees no students. Non-staff are left to their
93
+ * own search key, which already limits them to their own records.
94
+ *
95
+ * @param {UserData} user the signed-in user.
96
+ * @return {SearchStudentScope} the students they may find.
97
+ */
98
+ export declare const searchStudentScope: (user: UserData) => SearchStudentScope;
99
+ /**
100
+ * The cohorts whose placements a staff member may find, by the placement page's
101
+ * rule (canViewPlacement in ViewPlacement.tsx).
102
+ *
103
+ * @param {UserData} user the signed-in user.
104
+ * @return {"all"|"none"|string[]} every cohort, none, or these cohort ids.
105
+ */
106
+ export declare const searchPlacementCohorts: (user: UserData) => "all" | "none" | string[];
107
+ /**
108
+ * Keeps the placement hits whose student is inside a "some" student scope. The
109
+ * placements index holds the student's uid but not their details, so the filter
110
+ * can't do this; each student is read once. A hit with no student, or whose
111
+ * student can't be read, is dropped rather than shown unchecked.
112
+ *
113
+ * @param {any[]} hits placement hits from Algolia.
114
+ * @param {UserData} user the signed-in user.
115
+ * @return {Promise<any[]>} the hits they may see, in order.
116
+ */
117
+ export declare const keepPlacementHitsInStudentScope: (hits: any[], user: UserData) => Promise<any[]>;
75
118
  /**
76
119
  * The oIds whose employer databases this institute may read.
77
120
  *
@@ -16,12 +16,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  return (mod && mod.__esModule) ? mod : { "default": mod };
17
17
  };
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.nonEmptyGlobalSearchSections = exports.totalGlobalSearchHits = exports.globalSearch = exports.availableGlobalSearchIndexes = exports.GLOBAL_SEARCH_INDEXES = exports.searchLinkedSchools = exports.employerSearchScopes = exports.GLOBAL_SEARCH_INDEX_HITS = exports.GLOBAL_SEARCH_OVERVIEW_HITS = void 0;
19
+ exports.nonEmptyGlobalSearchSections = exports.totalGlobalSearchHits = exports.globalSearch = exports.availableGlobalSearchIndexes = exports.GLOBAL_SEARCH_INDEXES = exports.searchLinkedSchools = exports.employerSearchScopes = exports.keepPlacementHitsInStudentScope = exports.searchPlacementCohorts = exports.searchStudentScope = exports.GLOBAL_SEARCH_INDEX_HITS = exports.GLOBAL_SEARCH_OVERVIEW_HITS = void 0;
20
20
  const algoliasearch_1 = __importDefault(require("algoliasearch"));
21
21
  const util_1 = require("../../firebase/util");
22
22
  const algoliaConfig_1 = require("../../algoliaConfig");
23
23
  const productResolver_1 = require("../../productResolver");
24
24
  const sectors_1 = require("../../sectors/sectors");
25
+ const firebaseQuery_1 = __importDefault(require("../../firebase/firebaseQuery"));
25
26
  /** How many results a section shows before the user drills into one index. */
26
27
  exports.GLOBAL_SEARCH_OVERVIEW_HITS = 5;
27
28
  /** How many results one index shows per page once the user has drilled in. */
@@ -35,6 +36,66 @@ const matchesEveryTerm = (text, query) => {
35
36
  };
36
37
  /** Packages that run their own students, placements and cohorts. */
37
38
  const isSchoolPackage = (institute) => institute?.package === "institutes-one" || institute?.package === "institutes-two";
39
+ /**
40
+ * The students a staff member may find, by the Students list's rule: admins and
41
+ * "all" see everyone, "some" sees students whose `details[studentFilter]` is one of
42
+ * `studentFilterValues`, anyone else sees no students. Non-staff are left to their
43
+ * own search key, which already limits them to their own records.
44
+ *
45
+ * @param {UserData} user the signed-in user.
46
+ * @return {SearchStudentScope} the students they may find.
47
+ */
48
+ const searchStudentScope = (user) => {
49
+ if (user.userType !== "Staff" || user.userGroup === "admin" || user.viewStudents === "all")
50
+ return { kind: "all" };
51
+ if (user.viewStudents === "some" && user.studentFilter && user.studentFilterValues?.length) {
52
+ return { kind: "some", field: user.studentFilter, values: user.studentFilterValues };
53
+ }
54
+ return { kind: "none" };
55
+ };
56
+ exports.searchStudentScope = searchStudentScope;
57
+ /**
58
+ * The cohorts whose placements a staff member may find, by the placement page's
59
+ * rule (canViewPlacement in ViewPlacement.tsx).
60
+ *
61
+ * @param {UserData} user the signed-in user.
62
+ * @return {"all"|"none"|string[]} every cohort, none, or these cohort ids.
63
+ */
64
+ const searchPlacementCohorts = (user) => {
65
+ if (user.userType !== "Staff" || user.userGroup === "admin")
66
+ return "all";
67
+ if (user.viewCohorts === "some")
68
+ return user.visibleCohorts?.length ? user.visibleCohorts : "none";
69
+ if (user.viewCohorts === "all" && user.viewStudents !== "none")
70
+ return "all";
71
+ return "none";
72
+ };
73
+ exports.searchPlacementCohorts = searchPlacementCohorts;
74
+ /**
75
+ * Keeps the placement hits whose student is inside a "some" student scope. The
76
+ * placements index holds the student's uid but not their details, so the filter
77
+ * can't do this; each student is read once. A hit with no student, or whose
78
+ * student can't be read, is dropped rather than shown unchecked.
79
+ *
80
+ * @param {any[]} hits placement hits from Algolia.
81
+ * @param {UserData} user the signed-in user.
82
+ * @return {Promise<any[]>} the hits they may see, in order.
83
+ */
84
+ const keepPlacementHitsInStudentScope = async (hits, user) => {
85
+ const scope = (0, exports.searchStudentScope)(user);
86
+ if (scope.kind === "all")
87
+ return hits;
88
+ if (scope.kind === "none")
89
+ return [];
90
+ const firebaseQuery = new firebaseQuery_1.default();
91
+ const uids = Array.from(new Set(hits.map((hit) => hit.uid).filter(Boolean)));
92
+ const inScope = Object.fromEntries(await Promise.all(uids.map(async (uid) => {
93
+ const student = await firebaseQuery.getDocData(["users", uid]).catch(() => undefined);
94
+ return [uid, Boolean(student && scope.values.includes(String(student.details?.[scope.field] ?? "")))];
95
+ })));
96
+ return hits.filter((hit) => hit.uid && inScope[hit.uid]);
97
+ };
98
+ exports.keepPlacementHitsInStudentScope = keepPlacementHitsInStudentScope;
38
99
  /** Packages that keep an employer database of their own. */
39
100
  const hasEmployerDatabase = (institute) => isSchoolPackage(institute) || institute?.package === "careersHub";
40
101
  /**
@@ -108,8 +169,13 @@ exports.GLOBAL_SEARCH_INDEXES = [
108
169
  label: "Placements",
109
170
  labelSingular: "placement",
110
171
  indexName: "placements",
111
- available: (institute) => (0, productResolver_1.resolveProducts)(institute).wex,
112
- filters: (user) => `oId:${(0, util_1.quoteAlgoliaFilterIfNeeded)(user.oId)} AND draft:false`,
172
+ available: (institute, user) => (0, productResolver_1.resolveProducts)(institute).wex && (!user || (0, exports.searchPlacementCohorts)(user) !== "none"),
173
+ filters: (user) => {
174
+ const cohorts = (0, exports.searchPlacementCohorts)(user);
175
+ const cohortClause = Array.isArray(cohorts) ? ` AND (${cohorts.map((c) => `cohort:${(0, util_1.quoteAlgoliaFilterIfNeeded)(c)}`).join(" OR ")})` : "";
176
+ return `oId:${(0, util_1.quoteAlgoliaFilterIfNeeded)(user.oId)} AND draft:false${cohortClause}`;
177
+ },
178
+ keepHits: exports.keepPlacementHitsInStudentScope,
113
179
  toHit: (hit) => ({
114
180
  id: hit.objectID,
115
181
  title: hit.title || hit.name || "Untitled placement",
@@ -147,8 +213,15 @@ exports.GLOBAL_SEARCH_INDEXES = [
147
213
  label: "Students",
148
214
  labelSingular: "student",
149
215
  indexName: "users",
150
- available: (institute) => isSchoolPackage(institute),
151
- filters: (user) => `oId:${(0, util_1.quoteAlgoliaFilterIfNeeded)(user.oId)} AND userType:Students`,
216
+ available: (institute, user) => isSchoolPackage(institute) && (!user || (0, exports.searchStudentScope)(user).kind !== "none"),
217
+ // Same clause the Students list's table search builds. The search key itself
218
+ // isn't scoped yet, so this narrows what's shown rather than what's allowed.
219
+ filters: (user) => {
220
+ const scope = (0, exports.searchStudentScope)(user);
221
+ const scopeClause = scope.kind === "some" ?
222
+ ` AND (${scope.values.map((v) => `details.${scope.field}:${(0, util_1.quoteAlgoliaFilterIfNeeded)(v)}`).join(" OR ")})` : "";
223
+ return `oId:${(0, util_1.quoteAlgoliaFilterIfNeeded)(user.oId)} AND userType:Students${scopeClause}`;
224
+ },
152
225
  toHit: (hit) => ({
153
226
  id: hit.objectID,
154
227
  title: fullName(hit.details?.forename, hit.details?.surname) || hit.email || "Unnamed student",
@@ -219,6 +292,15 @@ const globalSearch = async (params) => {
219
292
  return undefined;
220
293
  }) :
221
294
  [];
295
+ // Drop hits an index's filter couldn't. The count only knows about this page's
296
+ // drops, so further pages can still hold some the total includes.
297
+ const keptResults = await Promise.all(algoliaResults.map(async (result, i) => {
298
+ const keepHits = algoliaIndexes[i].keepHits;
299
+ if (!result || !keepHits)
300
+ return result;
301
+ const hits = await keepHits(result.hits, user);
302
+ return { ...result, hits, nbHits: Math.max(hits.length, result.nbHits - (result.hits.length - hits.length)) };
303
+ }));
222
304
  const section = (index, hits, nbHits, nbPages, at) => ({
223
305
  id: index.id,
224
306
  label: index.label,
@@ -234,7 +316,7 @@ const globalSearch = async (params) => {
234
316
  const start = page * hitsPerPage;
235
317
  return [index.id, section(index, matches.slice(start, start + hitsPerPage), matches.length, Math.ceil(matches.length / hitsPerPage), page)];
236
318
  }
237
- const result = algoliaResults[algoliaIndexes.indexOf(index)];
319
+ const result = keptResults[algoliaIndexes.indexOf(index)];
238
320
  return [index.id, section(index, (result?.hits ?? []).map(index.toHit), result?.nbHits ?? 0, result?.nbPages ?? 0, result?.page ?? page)];
239
321
  }));
240
322
  };
@@ -1 +1 @@
1
- {"version":3,"file":"globalSearch.js","sourceRoot":"","sources":["../../../src/features/search/globalSearch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"globalSearch.js","sourceRoot":"","sources":["../../../src/features/search/globalSearch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * globalSearch.test.ts — the navbar search respects a staff member's student and
3
+ * cohort scope (QA 4.2.4).
4
+ */
5
+ export {};
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * globalSearch.test.ts — the navbar search respects a staff member's student and
4
+ * cohort scope (QA 4.2.4).
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ jest.mock("firebase/firestore", () => ({ where: jest.fn(), Timestamp: { now: jest.fn(), fromDate: jest.fn() } }));
8
+ jest.mock("firebase/storage", () => ({ getDownloadURL: jest.fn(), ref: jest.fn() }));
9
+ jest.mock("../../firebase/firebaseConfig", () => ({ db: {}, storage: {}, auth: {}, functions: {} }));
10
+ const mockGetDocData = jest.fn();
11
+ jest.mock("../../firebase/firebaseQuery", () => ({
12
+ __esModule: true,
13
+ default: jest.fn().mockImplementation(() => ({ getDocData: mockGetDocData })),
14
+ }));
15
+ const mockSearch = jest.fn();
16
+ jest.mock("algoliasearch", () => jest.fn(() => ({ initIndex: jest.fn((name) => ({ search: (q, o) => mockSearch(name, q, o) })) })));
17
+ const globalSearch_1 = require("./globalSearch");
18
+ const school = { package: "institutes-one", workExperience: true };
19
+ const staff = (u = {}) => ({
20
+ oId: "sch1", userType: "Staff", product: "institutes", userGroup: "staffGroup", algoliaKey: "key",
21
+ viewCohorts: "all", viewStudents: "all", ...u,
22
+ });
23
+ const scoped = staff({ viewStudents: "some", studentFilter: "year", studentFilterValues: ["10", "11"] });
24
+ const filtersFor = (id, user) => {
25
+ const index = globalSearch_1.GLOBAL_SEARCH_INDEXES.find((i) => i.id === id);
26
+ return index.filters(user);
27
+ };
28
+ beforeEach(() => {
29
+ mockGetDocData.mockReset();
30
+ mockSearch.mockReset();
31
+ });
32
+ describe("searchStudentScope", () => {
33
+ it("admins, staff who see all students, and non-staff are unscoped", () => {
34
+ expect((0, globalSearch_1.searchStudentScope)(staff({ userGroup: "admin", viewStudents: "none" })).kind).toBe("all");
35
+ expect((0, globalSearch_1.searchStudentScope)(staff()).kind).toBe("all");
36
+ expect((0, globalSearch_1.searchStudentScope)({ ...scoped, userType: "Students" }).kind).toBe("all");
37
+ });
38
+ it("'some' with a field and values is scoped to them", () => {
39
+ expect((0, globalSearch_1.searchStudentScope)(scoped)).toEqual({ kind: "some", field: "year", values: ["10", "11"] });
40
+ });
41
+ it("'none', unset, or 'some' with no values sees no students", () => {
42
+ expect((0, globalSearch_1.searchStudentScope)(staff({ viewStudents: "none" })).kind).toBe("none");
43
+ expect((0, globalSearch_1.searchStudentScope)(staff({ viewStudents: undefined })).kind).toBe("none");
44
+ expect((0, globalSearch_1.searchStudentScope)(staff({ viewStudents: "some", studentFilter: "year", studentFilterValues: [] })).kind).toBe("none");
45
+ });
46
+ });
47
+ describe("searchPlacementCohorts", () => {
48
+ it("matches the placement page's cohort rule", () => {
49
+ expect((0, globalSearch_1.searchPlacementCohorts)(staff({ userGroup: "admin", viewCohorts: undefined }))).toBe("all");
50
+ expect((0, globalSearch_1.searchPlacementCohorts)(staff())).toBe("all");
51
+ expect((0, globalSearch_1.searchPlacementCohorts)(staff({ viewCohorts: "some", visibleCohorts: ["c1", "c2"] }))).toEqual(["c1", "c2"]);
52
+ expect((0, globalSearch_1.searchPlacementCohorts)(staff({ viewCohorts: "some", visibleCohorts: [] }))).toBe("none");
53
+ expect((0, globalSearch_1.searchPlacementCohorts)(staff({ viewStudents: "none" }))).toBe("none");
54
+ expect((0, globalSearch_1.searchPlacementCohorts)(staff({ viewCohorts: undefined }))).toBe("none");
55
+ });
56
+ });
57
+ describe("filters sent to Algolia", () => {
58
+ it("students: unchanged for unscoped staff, narrowed for scoped staff", () => {
59
+ expect(filtersFor("students", staff())).toBe("oId:sch1 AND userType:Students");
60
+ expect(filtersFor("students", scoped)).toBe("oId:sch1 AND userType:Students AND (details.year:10 OR details.year:11)");
61
+ });
62
+ it("students: values with spaces are quoted", () => {
63
+ const formScoped = staff({ viewStudents: "some", studentFilter: "form", studentFilterValues: ["10 A"] });
64
+ expect(filtersFor("students", formScoped)).toBe("oId:sch1 AND userType:Students AND (details.form:'10 A')");
65
+ });
66
+ it("placements: unchanged for all cohorts, narrowed to visible cohorts", () => {
67
+ expect(filtersFor("placements", staff())).toBe("oId:sch1 AND draft:false");
68
+ expect(filtersFor("placements", staff({ viewCohorts: "some", visibleCohorts: ["c1", "c2"] })))
69
+ .toBe("oId:sch1 AND draft:false AND (cohort:c1 OR cohort:c2)");
70
+ });
71
+ });
72
+ describe("availableGlobalSearchIndexes", () => {
73
+ const ids = (user) => (0, globalSearch_1.availableGlobalSearchIndexes)(school, user).map((i) => i.id);
74
+ it("hides Students from staff who can't view students, and Placements from staff who can't view cohorts", () => {
75
+ expect(ids(staff())).toEqual(expect.arrayContaining(["students", "placements"]));
76
+ expect(ids(staff({ viewStudents: "none" }))).not.toContain("students");
77
+ expect(ids(staff({ viewStudents: "none" }))).not.toContain("placements");
78
+ expect(ids(staff({ viewCohorts: "some", visibleCohorts: [] }))).not.toContain("placements");
79
+ expect(ids(scoped)).toEqual(expect.arrayContaining(["students", "placements"]));
80
+ });
81
+ });
82
+ describe("keepPlacementHitsInStudentScope", () => {
83
+ const students = {
84
+ s10: { details: { year: "10" } },
85
+ s9: { details: { year: "9" } },
86
+ };
87
+ beforeEach(() => mockGetDocData.mockImplementation(async ([, uid]) => {
88
+ if (!students[uid])
89
+ throw new Error("not found");
90
+ return students[uid];
91
+ }));
92
+ it("keeps only placements of in-scope students, in order, reading each student once", async () => {
93
+ const hits = [{ objectID: "p1", uid: "s10" }, { objectID: "p2", uid: "s9" }, { objectID: "p3", uid: "s10" }, { objectID: "p4" }, { objectID: "p5", uid: "gone" }];
94
+ const kept = await (0, globalSearch_1.keepPlacementHitsInStudentScope)(hits, scoped);
95
+ expect(kept.map((h) => h.objectID)).toEqual(["p1", "p3"]);
96
+ expect(mockGetDocData).toHaveBeenCalledTimes(3); // s10, s9, gone
97
+ });
98
+ it("leaves hits alone for unscoped staff without reading anything", async () => {
99
+ const hits = [{ objectID: "p1", uid: "s9" }];
100
+ expect(await (0, globalSearch_1.keepPlacementHitsInStudentScope)(hits, staff())).toEqual(hits);
101
+ expect(mockGetDocData).not.toHaveBeenCalled();
102
+ });
103
+ });
104
+ describe("globalSearch", () => {
105
+ it("sends the scoped filters and drops out-of-scope placements from the results and count", async () => {
106
+ mockGetDocData.mockImplementation(async ([, uid]) => ({ details: { year: uid === "s10" ? "10" : "9" } }));
107
+ mockSearch.mockImplementation(async (index) => index === "placements" ?
108
+ { hits: [{ objectID: "p1", uid: "s10", name: "A" }, { objectID: "p2", uid: "s9", name: "B" }], nbHits: 2, page: 0, nbPages: 1 } :
109
+ { hits: [{ objectID: "u1", details: { forename: "In", surname: "Scope", year: "10" } }], nbHits: 1, page: 0, nbPages: 1 });
110
+ const results = await (0, globalSearch_1.globalSearch)({ user: scoped, institute: school, query: "finley", indexIds: ["students", "placements"] });
111
+ const sent = Object.fromEntries(mockSearch.mock.calls.map(([index, , options]) => [index, options.filters]));
112
+ expect(sent.users).toBe("oId:sch1 AND userType:Students AND (details.year:10 OR details.year:11)");
113
+ expect(sent.placements).toBe("oId:sch1 AND draft:false");
114
+ expect(results.placements?.hits.map((h) => h.id)).toEqual(["p1"]);
115
+ expect(results.placements?.nbHits).toBe(1);
116
+ expect(results.students?.nbHits).toBe(1);
117
+ });
118
+ });
119
+ //# sourceMappingURL=globalSearch.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globalSearch.test.js","sourceRoot":"","sources":["../../../src/features/search/globalSearch.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "private": false,
3
3
  "name": "placementt-core",
4
4
  "author": "Placementt",
5
- "version": "1.400.1249",
5
+ "version": "1.400.1250",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
8
  "scripts": {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * globalSearch.test.ts — the navbar search respects a staff member's student and
3
+ * cohort scope (QA 4.2.4).
4
+ */
5
+
6
+ jest.mock("firebase/firestore", () => ({where: jest.fn(), Timestamp: {now: jest.fn(), fromDate: jest.fn()}}));
7
+ jest.mock("firebase/storage", () => ({getDownloadURL: jest.fn(), ref: jest.fn()}));
8
+ jest.mock("../../firebase/firebaseConfig", () => ({db: {}, storage: {}, auth: {}, functions: {}}));
9
+
10
+ const mockGetDocData = jest.fn();
11
+ jest.mock("../../firebase/firebaseQuery", () => ({
12
+ __esModule: true,
13
+ default: jest.fn().mockImplementation(() => ({getDocData: mockGetDocData})),
14
+ }));
15
+
16
+ const mockSearch = jest.fn();
17
+ jest.mock("algoliasearch", () => jest.fn(() => ({initIndex: jest.fn((name: string) => ({search: (q: string, o: unknown) => mockSearch(name, q, o)}))})));
18
+
19
+ import {GLOBAL_SEARCH_INDEXES, availableGlobalSearchIndexes, globalSearch, keepPlacementHitsInStudentScope, searchPlacementCohorts, searchStudentScope} from "./globalSearch";
20
+ import {InstituteData, UserData} from "../../typeDefinitions";
21
+
22
+ const school = {package: "institutes-one", workExperience: true} as unknown as InstituteData;
23
+ const staff = (u: Partial<UserData> = {}) => ({
24
+ oId: "sch1", userType: "Staff", product: "institutes", userGroup: "staffGroup", algoliaKey: "key",
25
+ viewCohorts: "all", viewStudents: "all", ...u,
26
+ }) as UserData;
27
+ const scoped = staff({viewStudents: "some", studentFilter: "year", studentFilterValues: ["10", "11"]});
28
+ const filtersFor = (id: string, user: UserData) => {
29
+ const index = GLOBAL_SEARCH_INDEXES.find((i) => i.id === id) as {filters: (u: UserData) => string};
30
+ return index.filters(user);
31
+ };
32
+
33
+ beforeEach(() => {
34
+ mockGetDocData.mockReset();
35
+ mockSearch.mockReset();
36
+ });
37
+
38
+ describe("searchStudentScope", () => {
39
+ it("admins, staff who see all students, and non-staff are unscoped", () => {
40
+ expect(searchStudentScope(staff({userGroup: "admin", viewStudents: "none"})).kind).toBe("all");
41
+ expect(searchStudentScope(staff()).kind).toBe("all");
42
+ expect(searchStudentScope({...scoped, userType: "Students"} as UserData).kind).toBe("all");
43
+ });
44
+ it("'some' with a field and values is scoped to them", () => {
45
+ expect(searchStudentScope(scoped)).toEqual({kind: "some", field: "year", values: ["10", "11"]});
46
+ });
47
+ it("'none', unset, or 'some' with no values sees no students", () => {
48
+ expect(searchStudentScope(staff({viewStudents: "none"})).kind).toBe("none");
49
+ expect(searchStudentScope(staff({viewStudents: undefined})).kind).toBe("none");
50
+ expect(searchStudentScope(staff({viewStudents: "some", studentFilter: "year", studentFilterValues: []})).kind).toBe("none");
51
+ });
52
+ });
53
+
54
+ describe("searchPlacementCohorts", () => {
55
+ it("matches the placement page's cohort rule", () => {
56
+ expect(searchPlacementCohorts(staff({userGroup: "admin", viewCohorts: undefined}))).toBe("all");
57
+ expect(searchPlacementCohorts(staff())).toBe("all");
58
+ expect(searchPlacementCohorts(staff({viewCohorts: "some", visibleCohorts: ["c1", "c2"]}))).toEqual(["c1", "c2"]);
59
+ expect(searchPlacementCohorts(staff({viewCohorts: "some", visibleCohorts: []}))).toBe("none");
60
+ expect(searchPlacementCohorts(staff({viewStudents: "none"}))).toBe("none");
61
+ expect(searchPlacementCohorts(staff({viewCohorts: undefined}))).toBe("none");
62
+ });
63
+ });
64
+
65
+ describe("filters sent to Algolia", () => {
66
+ it("students: unchanged for unscoped staff, narrowed for scoped staff", () => {
67
+ expect(filtersFor("students", staff())).toBe("oId:sch1 AND userType:Students");
68
+ expect(filtersFor("students", scoped)).toBe("oId:sch1 AND userType:Students AND (details.year:10 OR details.year:11)");
69
+ });
70
+ it("students: values with spaces are quoted", () => {
71
+ const formScoped = staff({viewStudents: "some", studentFilter: "form", studentFilterValues: ["10 A"]});
72
+ expect(filtersFor("students", formScoped)).toBe("oId:sch1 AND userType:Students AND (details.form:'10 A')");
73
+ });
74
+ it("placements: unchanged for all cohorts, narrowed to visible cohorts", () => {
75
+ expect(filtersFor("placements", staff())).toBe("oId:sch1 AND draft:false");
76
+ expect(filtersFor("placements", staff({viewCohorts: "some", visibleCohorts: ["c1", "c2"]})))
77
+ .toBe("oId:sch1 AND draft:false AND (cohort:c1 OR cohort:c2)");
78
+ });
79
+ });
80
+
81
+ describe("availableGlobalSearchIndexes", () => {
82
+ const ids = (user: UserData) => availableGlobalSearchIndexes(school, user).map((i) => i.id);
83
+ it("hides Students from staff who can't view students, and Placements from staff who can't view cohorts", () => {
84
+ expect(ids(staff())).toEqual(expect.arrayContaining(["students", "placements"]));
85
+ expect(ids(staff({viewStudents: "none"}))).not.toContain("students");
86
+ expect(ids(staff({viewStudents: "none"}))).not.toContain("placements");
87
+ expect(ids(staff({viewCohorts: "some", visibleCohorts: []}))).not.toContain("placements");
88
+ expect(ids(scoped)).toEqual(expect.arrayContaining(["students", "placements"]));
89
+ });
90
+ });
91
+
92
+ describe("keepPlacementHitsInStudentScope", () => {
93
+ const students: {[uid: string]: UserData} = {
94
+ s10: {details: {year: "10"}} as unknown as UserData,
95
+ s9: {details: {year: "9"}} as unknown as UserData,
96
+ };
97
+ beforeEach(() => mockGetDocData.mockImplementation(async ([, uid]: string[]) => {
98
+ if (!students[uid]) throw new Error("not found");
99
+ return students[uid];
100
+ }));
101
+
102
+ it("keeps only placements of in-scope students, in order, reading each student once", async () => {
103
+ const hits = [{objectID: "p1", uid: "s10"}, {objectID: "p2", uid: "s9"}, {objectID: "p3", uid: "s10"}, {objectID: "p4"}, {objectID: "p5", uid: "gone"}];
104
+ const kept = await keepPlacementHitsInStudentScope(hits, scoped);
105
+ expect(kept.map((h) => h.objectID)).toEqual(["p1", "p3"]);
106
+ expect(mockGetDocData).toHaveBeenCalledTimes(3); // s10, s9, gone
107
+ });
108
+ it("leaves hits alone for unscoped staff without reading anything", async () => {
109
+ const hits = [{objectID: "p1", uid: "s9"}];
110
+ expect(await keepPlacementHitsInStudentScope(hits, staff())).toEqual(hits);
111
+ expect(mockGetDocData).not.toHaveBeenCalled();
112
+ });
113
+ });
114
+
115
+ describe("globalSearch", () => {
116
+ it("sends the scoped filters and drops out-of-scope placements from the results and count", async () => {
117
+ mockGetDocData.mockImplementation(async ([, uid]: string[]) => ({details: {year: uid === "s10" ? "10" : "9"}}));
118
+ mockSearch.mockImplementation(async (index: string) => index === "placements" ?
119
+ {hits: [{objectID: "p1", uid: "s10", name: "A"}, {objectID: "p2", uid: "s9", name: "B"}], nbHits: 2, page: 0, nbPages: 1} :
120
+ {hits: [{objectID: "u1", details: {forename: "In", surname: "Scope", year: "10"}}], nbHits: 1, page: 0, nbPages: 1});
121
+
122
+ const results = await globalSearch({user: scoped, institute: school, query: "finley", indexIds: ["students", "placements"]});
123
+
124
+ const sent = Object.fromEntries(mockSearch.mock.calls.map(([index, , options]) => [index, (options as {filters: string}).filters]));
125
+ expect(sent.users).toBe("oId:sch1 AND userType:Students AND (details.year:10 OR details.year:11)");
126
+ expect(sent.placements).toBe("oId:sch1 AND draft:false");
127
+ expect(results.placements?.hits.map((h) => h.id)).toEqual(["p1"]);
128
+ expect(results.placements?.nbHits).toBe(1);
129
+ expect(results.students?.nbHits).toBe(1);
130
+ });
131
+ });
@@ -18,6 +18,7 @@ import {capitaliseWords, quoteAlgoliaFilterIfNeeded} from "../../firebase/util";
18
18
  import {ALGOLIA_APP_ID} from "../../algoliaConfig";
19
19
  import {resolveProducts} from "../../productResolver";
20
20
  import {formatSectors} from "../../sectors/sectors";
21
+ import FirebaseQuery from "../../firebase/firebaseQuery";
21
22
 
22
23
  /** How many results a section shows before the user drills into one index. */
23
24
  export const GLOBAL_SEARCH_OVERVIEW_HITS = 5;
@@ -74,6 +75,11 @@ export type GlobalSearchIndexDefinition = {
74
75
  filters: (user: UserData, institute?: InstituteData) => string,
75
76
  /** Turns one raw hit into a row. */
76
77
  toHit: (hit: any) => GlobalSearchHit,
78
+ /**
79
+ * Drops hits the filter couldn't, for records that don't carry what the user's
80
+ * access depends on (a placement holds its student's uid, not their details).
81
+ */
82
+ keepHits?: (hits: any[], user: UserData) => Promise<any[]>,
77
83
  } | {
78
84
  /**
79
85
  * Searched in memory, from records the caller already holds, instead of
@@ -98,6 +104,62 @@ const matchesEveryTerm = (text: string, query: string) => {
98
104
  const isSchoolPackage = (institute?: InstituteData) =>
99
105
  institute?.package === "institutes-one" || institute?.package === "institutes-two";
100
106
 
107
+ export type SearchStudentScope = {kind: "all"} | {kind: "none"} | {kind: "some", field: string, values: string[]};
108
+
109
+ /**
110
+ * The students a staff member may find, by the Students list's rule: admins and
111
+ * "all" see everyone, "some" sees students whose `details[studentFilter]` is one of
112
+ * `studentFilterValues`, anyone else sees no students. Non-staff are left to their
113
+ * own search key, which already limits them to their own records.
114
+ *
115
+ * @param {UserData} user the signed-in user.
116
+ * @return {SearchStudentScope} the students they may find.
117
+ */
118
+ export const searchStudentScope = (user: UserData): SearchStudentScope => {
119
+ if (user.userType !== "Staff" || user.userGroup === "admin" || user.viewStudents === "all") return {kind: "all"};
120
+ if (user.viewStudents === "some" && user.studentFilter && user.studentFilterValues?.length) {
121
+ return {kind: "some", field: user.studentFilter, values: user.studentFilterValues};
122
+ }
123
+ return {kind: "none"};
124
+ };
125
+
126
+ /**
127
+ * The cohorts whose placements a staff member may find, by the placement page's
128
+ * rule (canViewPlacement in ViewPlacement.tsx).
129
+ *
130
+ * @param {UserData} user the signed-in user.
131
+ * @return {"all"|"none"|string[]} every cohort, none, or these cohort ids.
132
+ */
133
+ export const searchPlacementCohorts = (user: UserData): "all"|"none"|string[] => {
134
+ if (user.userType !== "Staff" || user.userGroup === "admin") return "all";
135
+ if (user.viewCohorts === "some") return user.visibleCohorts?.length ? user.visibleCohorts : "none";
136
+ if (user.viewCohorts === "all" && user.viewStudents !== "none") return "all";
137
+ return "none";
138
+ };
139
+
140
+ /**
141
+ * Keeps the placement hits whose student is inside a "some" student scope. The
142
+ * placements index holds the student's uid but not their details, so the filter
143
+ * can't do this; each student is read once. A hit with no student, or whose
144
+ * student can't be read, is dropped rather than shown unchecked.
145
+ *
146
+ * @param {any[]} hits placement hits from Algolia.
147
+ * @param {UserData} user the signed-in user.
148
+ * @return {Promise<any[]>} the hits they may see, in order.
149
+ */
150
+ export const keepPlacementHitsInStudentScope = async (hits: any[], user: UserData): Promise<any[]> => {
151
+ const scope = searchStudentScope(user);
152
+ if (scope.kind === "all") return hits;
153
+ if (scope.kind === "none") return [];
154
+ const firebaseQuery = new FirebaseQuery();
155
+ const uids = Array.from(new Set(hits.map((hit) => hit.uid).filter(Boolean))) as string[];
156
+ const inScope = Object.fromEntries(await Promise.all(uids.map(async (uid) => {
157
+ const student = await firebaseQuery.getDocData(["users", uid]).catch(() => undefined) as UserData|undefined;
158
+ return [uid, Boolean(student && scope.values.includes(String(student.details?.[scope.field] ?? "")))];
159
+ })));
160
+ return hits.filter((hit) => hit.uid && inScope[hit.uid]);
161
+ };
162
+
101
163
  /** Packages that keep an employer database of their own. */
102
164
  const hasEmployerDatabase = (institute?: InstituteData) =>
103
165
  isSchoolPackage(institute) || institute?.package === "careersHub";
@@ -180,8 +242,13 @@ export const GLOBAL_SEARCH_INDEXES: GlobalSearchIndexDefinition[] = [
180
242
  label: "Placements",
181
243
  labelSingular: "placement",
182
244
  indexName: "placements",
183
- available: (institute) => resolveProducts(institute).wex,
184
- filters: (user) => `oId:${quoteAlgoliaFilterIfNeeded(user.oId)} AND draft:false`,
245
+ available: (institute, user) => resolveProducts(institute).wex && (!user || searchPlacementCohorts(user) !== "none"),
246
+ filters: (user) => {
247
+ const cohorts = searchPlacementCohorts(user);
248
+ const cohortClause = Array.isArray(cohorts) ? ` AND (${cohorts.map((c) => `cohort:${quoteAlgoliaFilterIfNeeded(c)}`).join(" OR ")})` : "";
249
+ return `oId:${quoteAlgoliaFilterIfNeeded(user.oId)} AND draft:false${cohortClause}`;
250
+ },
251
+ keepHits: keepPlacementHitsInStudentScope,
185
252
  toHit: (hit) => ({
186
253
  id: hit.objectID,
187
254
  title: hit.title || hit.name || "Untitled placement",
@@ -226,8 +293,15 @@ export const GLOBAL_SEARCH_INDEXES: GlobalSearchIndexDefinition[] = [
226
293
  label: "Students",
227
294
  labelSingular: "student",
228
295
  indexName: "users",
229
- available: (institute) => isSchoolPackage(institute),
230
- filters: (user) => `oId:${quoteAlgoliaFilterIfNeeded(user.oId)} AND userType:Students`,
296
+ available: (institute, user) => isSchoolPackage(institute) && (!user || searchStudentScope(user).kind !== "none"),
297
+ // Same clause the Students list's table search builds. The search key itself
298
+ // isn't scoped yet, so this narrows what's shown rather than what's allowed.
299
+ filters: (user) => {
300
+ const scope = searchStudentScope(user);
301
+ const scopeClause = scope.kind === "some" ?
302
+ ` AND (${scope.values.map((v) => `details.${scope.field}:${quoteAlgoliaFilterIfNeeded(v)}`).join(" OR ")})` : "";
303
+ return `oId:${quoteAlgoliaFilterIfNeeded(user.oId)} AND userType:Students${scopeClause}`;
304
+ },
231
305
  toHit: (hit) => ({
232
306
  id: hit.objectID,
233
307
  title: fullName(hit.details?.forename, hit.details?.surname) || hit.email || "Unnamed student",
@@ -322,6 +396,15 @@ export const globalSearch = async (params: GlobalSearchParams): Promise<GlobalSe
322
396
  }) :
323
397
  [];
324
398
 
399
+ // Drop hits an index's filter couldn't. The count only knows about this page's
400
+ // drops, so further pages can still hold some the total includes.
401
+ const keptResults = await Promise.all(algoliaResults.map(async (result, i) => {
402
+ const keepHits = algoliaIndexes[i].keepHits;
403
+ if (!result || !keepHits) return result;
404
+ const hits = await keepHits(result.hits, user);
405
+ return {...result, hits, nbHits: Math.max(hits.length, result.nbHits - (result.hits.length - hits.length))};
406
+ }));
407
+
325
408
  const section = (index: GlobalSearchIndexDefinition, hits: GlobalSearchHit[], nbHits: number, nbPages: number, at: number): GlobalSearchIndexResults => ({
326
409
  id: index.id,
327
410
  label: index.label,
@@ -338,7 +421,7 @@ export const globalSearch = async (params: GlobalSearchParams): Promise<GlobalSe
338
421
  const start = page * hitsPerPage;
339
422
  return [index.id, section(index, matches.slice(start, start + hitsPerPage), matches.length, Math.ceil(matches.length / hitsPerPage), page)];
340
423
  }
341
- const result = algoliaResults[algoliaIndexes.indexOf(index)] as {hits?: any[], nbHits?: number, page?: number, nbPages?: number}|undefined;
424
+ const result = keptResults[algoliaIndexes.indexOf(index)] as {hits?: any[], nbHits?: number, page?: number, nbPages?: number}|undefined;
342
425
  return [index.id, section(index, (result?.hits ?? []).map(index.toHit), result?.nbHits ?? 0, result?.nbPages ?? 0, result?.page ?? page)];
343
426
  })) as GlobalSearchResults;
344
427
  };