@studious-lms/server 1.17.0 → 1.17.1
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/dist/models/tag.d.ts +34 -0
- package/dist/models/tag.d.ts.map +1 -1
- package/dist/models/tag.js +18 -2
- package/dist/models/tag.js.map +1 -1
- package/dist/services/tag.d.ts.map +1 -1
- package/dist/services/tag.js +40 -5
- package/dist/services/tag.js.map +1 -1
- package/package.json +1 -1
- package/src/models/tag.ts +21 -0
- package/src/services/tag.ts +39 -2
package/dist/models/tag.d.ts
CHANGED
|
@@ -43,6 +43,40 @@ export declare function getTag(tagId: string): import("@prisma/client/runtime/li
|
|
|
43
43
|
color: string;
|
|
44
44
|
departmentId: string | null;
|
|
45
45
|
} | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Finds a tag in the same school whose name matches case-insensitively, used to
|
|
48
|
+
* keep tag names normalised (no "Honors" / "honors" duplicates). Pass
|
|
49
|
+
* `excludeTagId` when checking during an update so the tag doesn't clash with
|
|
50
|
+
* itself.
|
|
51
|
+
*/
|
|
52
|
+
export declare function findTagByName(schoolId: string, name: string, excludeTagId?: string): import("@prisma/client/runtime/library.js").DynamicModelExtensionFluentApi<import(".prisma/client").Prisma.TypeMap<import("@prisma/client/runtime/library.js").InternalArgs & {
|
|
53
|
+
result: {
|
|
54
|
+
userProfile: {
|
|
55
|
+
profilePicture: () => {
|
|
56
|
+
needs: {
|
|
57
|
+
profilePicture: true;
|
|
58
|
+
};
|
|
59
|
+
compute(profile: {
|
|
60
|
+
profilePicture: string | null;
|
|
61
|
+
}): string | null;
|
|
62
|
+
};
|
|
63
|
+
profilePictureThumbnail: () => {
|
|
64
|
+
needs: {
|
|
65
|
+
profilePictureThumbnail: true;
|
|
66
|
+
};
|
|
67
|
+
compute(profile: {
|
|
68
|
+
profilePictureThumbnail: string | null;
|
|
69
|
+
}): string | null;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
model: {};
|
|
74
|
+
query: {};
|
|
75
|
+
client: {};
|
|
76
|
+
}, {}>, "Tag", "findFirst", null> & import("@prisma/client/runtime/library.js").PrismaPromise<{
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
} | null>;
|
|
46
80
|
export declare function createTag(data: {
|
|
47
81
|
schoolId: string;
|
|
48
82
|
departmentId?: string | null;
|
package/dist/models/tag.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"/","sources":["models/tag.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEjE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;;;;;;;;KAc3C;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAKnC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAUA;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAOtE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAEtC;AASD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;;;;;;;;;;;;;;;;;;;;;;;;;;GASA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;;;;;;;;;;;;;;;;;;;;;;;;;;GASA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM;;;;;;;;KA2BrE"}
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"/","sources":["models/tag.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEjE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;;;;;;;;KAc3C;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAKnC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;UAUtB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAUA;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAOtE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAEtC;AASD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;;;;;;;;;;;;;;;;;;;;;;;;;;GASA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;;;;;;;;;;;;;;;;;;;;;;;;;;GASA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM;;;;;;;;KA2BrE"}
|
package/dist/models/tag.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* and grading boundaries. Optionally associated with a department.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
6
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="49abc52c-9751-5de2-a871-b58e5d925c9a")}catch(e){}}();
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
8
|
const tagSelect = {
|
|
9
9
|
id: true,
|
|
@@ -34,6 +34,22 @@ export function getTag(tagId) {
|
|
|
34
34
|
select: tagSelect,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Finds a tag in the same school whose name matches case-insensitively, used to
|
|
39
|
+
* keep tag names normalised (no "Honors" / "honors" duplicates). Pass
|
|
40
|
+
* `excludeTagId` when checking during an update so the tag doesn't clash with
|
|
41
|
+
* itself.
|
|
42
|
+
*/
|
|
43
|
+
export function findTagByName(schoolId, name, excludeTagId) {
|
|
44
|
+
return prisma.tag.findFirst({
|
|
45
|
+
where: {
|
|
46
|
+
schoolId,
|
|
47
|
+
name: { equals: name, mode: "insensitive" },
|
|
48
|
+
...(excludeTagId ? { id: { not: excludeTagId } } : {}),
|
|
49
|
+
},
|
|
50
|
+
select: { id: true, name: true },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
37
53
|
export function createTag(data) {
|
|
38
54
|
return prisma.tag.create({
|
|
39
55
|
data: {
|
|
@@ -110,4 +126,4 @@ export function listTagsAttachedTo(target, targetId) {
|
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
//# sourceMappingURL=tag.js.map
|
|
113
|
-
//# debugId=
|
|
129
|
+
//# debugId=49abc52c-9751-5de2-a871-b58e5d925c9a
|
package/dist/models/tag.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.js","sources":["models/tag.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Tag model — institution-scoped tags applicable to users, classes, rubrics,\n * and grading boundaries. Optionally associated with a department.\n */\nimport { prisma } from \"../lib/prisma.js\";\n\nconst tagSelect = {\n id: true,\n schoolId: true,\n departmentId: true,\n name: true,\n color: true,\n createdAt: true,\n updatedAt: true,\n} as const;\n\nexport type TagTarget = \"USER\" | \"CLASS\" | \"RUBRIC\" | \"BOUNDARY\";\n\nexport function listTagsForSchool(\n schoolId: string,\n options?: { departmentId?: string | null }\n) {\n return prisma.tag.findMany({\n where: {\n schoolId,\n ...(options?.departmentId === null\n ? { departmentId: null }\n : options?.departmentId\n ? { departmentId: options.departmentId }\n : {}),\n },\n orderBy: [{ name: \"asc\" }],\n select: tagSelect,\n });\n}\n\nexport function getTag(tagId: string) {\n return prisma.tag.findUnique({\n where: { id: tagId },\n select: tagSelect,\n });\n}\n\nexport function createTag(data: {\n schoolId: string;\n departmentId?: string | null;\n name: string;\n color?: string;\n}) {\n return prisma.tag.create({\n data: {\n schoolId: data.schoolId,\n departmentId: data.departmentId ?? null,\n name: data.name,\n color: data.color ?? \"#3B82F6\",\n },\n select: tagSelect,\n });\n}\n\nexport function updateTag(\n tagId: string,\n data: { name?: string; color?: string; departmentId?: string | null }\n) {\n return prisma.tag.update({\n where: { id: tagId },\n data,\n select: tagSelect,\n });\n}\n\nexport function deleteTag(tagId: string) {\n return prisma.tag.delete({ where: { id: tagId } });\n}\n\nconst targetRelation: Record<TagTarget, \"users\" | \"classes\" | \"rubrics\" | \"boundaries\"> = {\n USER: \"users\",\n CLASS: \"classes\",\n RUBRIC: \"rubrics\",\n BOUNDARY: \"boundaries\",\n};\n\nexport function attachTagToTarget(params: {\n tagId: string;\n target: TagTarget;\n targetId: string;\n}) {\n const relation = targetRelation[params.target];\n return prisma.tag.update({\n where: { id: params.tagId },\n data: {\n [relation]: { connect: { id: params.targetId } },\n },\n select: { id: true },\n });\n}\n\nexport function detachTagFromTarget(params: {\n tagId: string;\n target: TagTarget;\n targetId: string;\n}) {\n const relation = targetRelation[params.target];\n return prisma.tag.update({\n where: { id: params.tagId },\n data: {\n [relation]: { disconnect: { id: params.targetId } },\n },\n select: { id: true },\n });\n}\n\nexport function listTagsAttachedTo(target: TagTarget, targetId: string) {\n switch (target) {\n case \"USER\":\n return prisma.tag.findMany({\n where: { users: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n case \"CLASS\":\n return prisma.tag.findMany({\n where: { classes: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n case \"RUBRIC\":\n return prisma.tag.findMany({\n where: { rubrics: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n case \"BOUNDARY\":\n return prisma.tag.findMany({\n where: { boundaries: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"
|
|
1
|
+
{"version":3,"file":"tag.js","sources":["models/tag.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Tag model — institution-scoped tags applicable to users, classes, rubrics,\n * and grading boundaries. Optionally associated with a department.\n */\nimport { prisma } from \"../lib/prisma.js\";\n\nconst tagSelect = {\n id: true,\n schoolId: true,\n departmentId: true,\n name: true,\n color: true,\n createdAt: true,\n updatedAt: true,\n} as const;\n\nexport type TagTarget = \"USER\" | \"CLASS\" | \"RUBRIC\" | \"BOUNDARY\";\n\nexport function listTagsForSchool(\n schoolId: string,\n options?: { departmentId?: string | null }\n) {\n return prisma.tag.findMany({\n where: {\n schoolId,\n ...(options?.departmentId === null\n ? { departmentId: null }\n : options?.departmentId\n ? { departmentId: options.departmentId }\n : {}),\n },\n orderBy: [{ name: \"asc\" }],\n select: tagSelect,\n });\n}\n\nexport function getTag(tagId: string) {\n return prisma.tag.findUnique({\n where: { id: tagId },\n select: tagSelect,\n });\n}\n\n/**\n * Finds a tag in the same school whose name matches case-insensitively, used to\n * keep tag names normalised (no \"Honors\" / \"honors\" duplicates). Pass\n * `excludeTagId` when checking during an update so the tag doesn't clash with\n * itself.\n */\nexport function findTagByName(\n schoolId: string,\n name: string,\n excludeTagId?: string\n) {\n return prisma.tag.findFirst({\n where: {\n schoolId,\n name: { equals: name, mode: \"insensitive\" },\n ...(excludeTagId ? { id: { not: excludeTagId } } : {}),\n },\n select: { id: true, name: true },\n });\n}\n\nexport function createTag(data: {\n schoolId: string;\n departmentId?: string | null;\n name: string;\n color?: string;\n}) {\n return prisma.tag.create({\n data: {\n schoolId: data.schoolId,\n departmentId: data.departmentId ?? null,\n name: data.name,\n color: data.color ?? \"#3B82F6\",\n },\n select: tagSelect,\n });\n}\n\nexport function updateTag(\n tagId: string,\n data: { name?: string; color?: string; departmentId?: string | null }\n) {\n return prisma.tag.update({\n where: { id: tagId },\n data,\n select: tagSelect,\n });\n}\n\nexport function deleteTag(tagId: string) {\n return prisma.tag.delete({ where: { id: tagId } });\n}\n\nconst targetRelation: Record<TagTarget, \"users\" | \"classes\" | \"rubrics\" | \"boundaries\"> = {\n USER: \"users\",\n CLASS: \"classes\",\n RUBRIC: \"rubrics\",\n BOUNDARY: \"boundaries\",\n};\n\nexport function attachTagToTarget(params: {\n tagId: string;\n target: TagTarget;\n targetId: string;\n}) {\n const relation = targetRelation[params.target];\n return prisma.tag.update({\n where: { id: params.tagId },\n data: {\n [relation]: { connect: { id: params.targetId } },\n },\n select: { id: true },\n });\n}\n\nexport function detachTagFromTarget(params: {\n tagId: string;\n target: TagTarget;\n targetId: string;\n}) {\n const relation = targetRelation[params.target];\n return prisma.tag.update({\n where: { id: params.tagId },\n data: {\n [relation]: { disconnect: { id: params.targetId } },\n },\n select: { id: true },\n });\n}\n\nexport function listTagsAttachedTo(target: TagTarget, targetId: string) {\n switch (target) {\n case \"USER\":\n return prisma.tag.findMany({\n where: { users: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n case \"CLASS\":\n return prisma.tag.findMany({\n where: { classes: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n case \"RUBRIC\":\n return prisma.tag.findMany({\n where: { rubrics: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n case \"BOUNDARY\":\n return prisma.tag.findMany({\n where: { boundaries: { some: { id: targetId } } },\n select: tagSelect,\n orderBy: { name: \"asc\" },\n });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"49abc52c-9751-5de2-a871-b58e5d925c9a"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"/","sources":["services/tag.ts"],"names":[],"mappings":"AAMA,OAAO,
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"/","sources":["services/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EASL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAC;AA4C1B,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;;;;;;;;KAM1D;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IACL,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;GAwBF;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IACL,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;;;;;;;;GAmCF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;GAQlE;AA2ED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;;GAS9D;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;;GAQ9D;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;;;;;;;;KAO/C"}
|
package/dist/services/tag.js
CHANGED
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
* targets (users, classes, rubrics, boundaries).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
6
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b6385fba-13fc-519e-9095-49126706f392")}catch(e){}}();
|
|
7
7
|
import { TRPCError } from "@trpc/server";
|
|
8
8
|
import { prisma } from "../lib/prisma.js";
|
|
9
|
-
import { attachTagToTarget, createTag, deleteTag, detachTagFromTarget, getTag, listTagsAttachedTo, listTagsForSchool, updateTag, } from "../models/tag.js";
|
|
9
|
+
import { attachTagToTarget, createTag, deleteTag, detachTagFromTarget, findTagByName, getTag, listTagsAttachedTo, listTagsForSchool, updateTag, } from "../models/tag.js";
|
|
10
10
|
import { findActiveSchoolAdminMembership, findSchoolMembership, } from "../models/school.js";
|
|
11
|
+
/**
|
|
12
|
+
* Normalises a tag name to a canonical display form: trimmed with internal
|
|
13
|
+
* whitespace collapsed to single spaces. Casing is preserved so labels like
|
|
14
|
+
* "AP" or "Year 12" read naturally; uniqueness is enforced case-insensitively
|
|
15
|
+
* separately via {@link findTagByName}.
|
|
16
|
+
*/
|
|
17
|
+
function normalizeTagName(name) {
|
|
18
|
+
const normalized = name.trim().replace(/\s+/g, " ");
|
|
19
|
+
if (!normalized) {
|
|
20
|
+
throw new TRPCError({
|
|
21
|
+
code: "BAD_REQUEST",
|
|
22
|
+
message: "Tag name can't be empty",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
11
27
|
async function requireAdmin(schoolId, userId) {
|
|
12
28
|
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
13
29
|
if (!membership) {
|
|
@@ -47,7 +63,15 @@ export async function createTagRecord(userId, input) {
|
|
|
47
63
|
});
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
|
-
|
|
66
|
+
const name = normalizeTagName(input.name);
|
|
67
|
+
const existing = await findTagByName(input.schoolId, name);
|
|
68
|
+
if (existing) {
|
|
69
|
+
throw new TRPCError({
|
|
70
|
+
code: "CONFLICT",
|
|
71
|
+
message: `A tag named "${existing.name}" already exists in this institution`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return createTag({ ...input, name });
|
|
51
75
|
}
|
|
52
76
|
export async function updateTagRecord(userId, input) {
|
|
53
77
|
const tag = await getTag(input.tagId);
|
|
@@ -67,8 +91,19 @@ export async function updateTagRecord(userId, input) {
|
|
|
67
91
|
});
|
|
68
92
|
}
|
|
69
93
|
}
|
|
94
|
+
let name = input.name;
|
|
95
|
+
if (name !== undefined) {
|
|
96
|
+
name = normalizeTagName(name);
|
|
97
|
+
const existing = await findTagByName(tag.schoolId, name, input.tagId);
|
|
98
|
+
if (existing) {
|
|
99
|
+
throw new TRPCError({
|
|
100
|
+
code: "CONFLICT",
|
|
101
|
+
message: `A tag named "${existing.name}" already exists in this institution`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
70
105
|
return updateTag(input.tagId, {
|
|
71
|
-
name
|
|
106
|
+
name,
|
|
72
107
|
color: input.color,
|
|
73
108
|
departmentId: input.departmentId,
|
|
74
109
|
});
|
|
@@ -175,4 +210,4 @@ export async function getAttachedTags(userId, input) {
|
|
|
175
210
|
return tags;
|
|
176
211
|
}
|
|
177
212
|
//# sourceMappingURL=tag.js.map
|
|
178
|
-
//# debugId=
|
|
213
|
+
//# debugId=b6385fba-13fc-519e-9095-49126706f392
|
package/dist/services/tag.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.js","sources":["services/tag.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Tag service — institution-scoped tag CRUD and assignment to taggable\n * targets (users, classes, rubrics, boundaries).\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport {\n attachTagToTarget,\n createTag,\n deleteTag,\n detachTagFromTarget,\n getTag,\n listTagsAttachedTo,\n listTagsForSchool,\n type TagTarget,\n updateTag,\n} from \"../models/tag.js\";\nimport {\n findActiveSchoolAdminMembership,\n findSchoolMembership,\n} from \"../models/school.js\";\n\nasync function requireAdmin(schoolId: string, userId: string) {\n const membership = await findActiveSchoolAdminMembership(schoolId, userId);\n if (!membership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only school admins can manage tags\",\n });\n }\n}\n\nasync function requireMember(schoolId: string, userId: string) {\n const membership = await findSchoolMembership(schoolId, userId);\n if (!membership || membership.status !== \"ACTIVE\") {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You aren't a member of this institution\",\n });\n }\n return membership;\n}\n\nexport async function listTags(\n userId: string,\n input: { schoolId: string; departmentId?: string | null }\n) {\n await requireMember(input.schoolId, userId);\n return listTagsForSchool(input.schoolId, {\n departmentId: input.departmentId,\n });\n}\n\nexport async function createTagRecord(\n userId: string,\n input: {\n schoolId: string;\n departmentId?: string | null;\n name: string;\n color?: string;\n }\n) {\n await requireAdmin(input.schoolId, userId);\n if (input.departmentId) {\n const dept = await prisma.department.findUnique({\n where: { id: input.departmentId },\n select: { schoolId: true },\n });\n if (!dept || dept.schoolId !== input.schoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Department does not belong to this institution\",\n });\n }\n }\n return createTag(input);\n}\n\nexport async function updateTagRecord(\n userId: string,\n input: {\n tagId: string;\n name?: string;\n color?: string;\n departmentId?: string | null;\n }\n) {\n const tag = await getTag(input.tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireAdmin(tag.schoolId, userId);\n if (input.departmentId) {\n const dept = await prisma.department.findUnique({\n where: { id: input.departmentId },\n select: { schoolId: true },\n });\n if (!dept || dept.schoolId !== tag.schoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Department does not belong to this institution\",\n });\n }\n }\n return updateTag(input.tagId, {\n name: input.name,\n color: input.color,\n departmentId: input.departmentId,\n });\n}\n\nexport async function deleteTagRecord(userId: string, tagId: string) {\n const tag = await getTag(tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireAdmin(tag.schoolId, userId);\n await deleteTag(tagId);\n return { success: true };\n}\n\nasync function ensureTargetInScope(\n schoolId: string,\n target: TagTarget,\n targetId: string\n) {\n switch (target) {\n case \"USER\": {\n const inSchool = await prisma.schoolMembership.findFirst({\n where: { schoolId, userId: targetId, status: \"ACTIVE\" },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"User is not in this institution\",\n });\n }\n return;\n }\n case \"CLASS\": {\n const inSchool = await prisma.class.findFirst({\n where: { id: targetId, schoolId },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Class is not in this institution\",\n });\n }\n return;\n }\n case \"RUBRIC\": {\n const inSchool = await prisma.markScheme.findFirst({\n where: {\n id: targetId,\n OR: [\n { class: { schoolId } },\n { department: { schoolId } },\n ],\n },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Rubric is not in this institution\",\n });\n }\n return;\n }\n case \"BOUNDARY\": {\n const inSchool = await prisma.gradingBoundary.findFirst({\n where: {\n id: targetId,\n OR: [\n { class: { schoolId } },\n { department: { schoolId } },\n ],\n },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Boundary is not in this institution\",\n });\n }\n return;\n }\n }\n}\n\nexport async function attachTag(\n userId: string,\n input: { tagId: string; target: TagTarget; targetId: string }\n) {\n const tag = await getTag(input.tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireMember(tag.schoolId, userId);\n await ensureTargetInScope(tag.schoolId, input.target, input.targetId);\n return attachTagToTarget(input);\n}\n\nexport async function detachTag(\n userId: string,\n input: { tagId: string; target: TagTarget; targetId: string }\n) {\n const tag = await getTag(input.tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireMember(tag.schoolId, userId);\n return detachTagFromTarget(input);\n}\n\nexport async function getAttachedTags(\n userId: string,\n input: { target: TagTarget; targetId: string }\n) {\n const tags = await listTagsAttachedTo(input.target, input.targetId);\n if (tags[0]) {\n await requireMember(tags[0].schoolId, userId);\n }\n return tags;\n}\n"],"names":[],"mappings":"AAAA;;;GAGG;;;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,kBAAkB,EAClB,iBAAiB,EAEjB,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,+BAA+B,EAC/B,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,KAAK,UAAU,YAAY,CAAC,QAAgB,EAAE,MAAc;IAC1D,MAAM,UAAU,GAAG,MAAM,+BAA+B,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3E,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,oCAAoC;SAC9C,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,QAAgB,EAAE,MAAc;IAC3D,MAAM,UAAU,GAAG,MAAM,oBAAoB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAChE,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,yCAAyC;SACnD,CAAC,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAc,EACd,KAAyD;IAEzD,MAAM,aAAa,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5C,OAAO,iBAAiB,CAAC,KAAK,CAAC,QAAQ,EAAE;QACvC,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,KAKC;IAED,MAAM,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;YAC9C,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,YAAY,EAAE;YACjC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC9C,MAAM,IAAI,SAAS,CAAC;gBAClB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,gDAAgD;aAC1D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,KAKC;IAED,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;YAC9C,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,YAAY,EAAE;YACjC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ,EAAE,CAAC;YAC5C,MAAM,IAAI,SAAS,CAAC;gBAClB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,gDAAgD;aAC1D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE;QAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc,EAAE,KAAa;IACjE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;IACvB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,QAAgB,EAChB,MAAiB,EACjB,QAAgB;IAEhB,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,SAAS,CAAC;gBACvD,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE;gBACvD,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;aACrB,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,SAAS,CAAC;oBAClB,IAAI,EAAE,aAAa;oBACnB,OAAO,EAAE,iCAAiC;iBAC3C,CAAC,CAAC;YACL,CAAC;YACD,OAAO;QACT,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;gBAC5C,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBACjC,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;aACrB,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,SAAS,CAAC;oBAClB,IAAI,EAAE,aAAa;oBACnB,OAAO,EAAE,kCAAkC;iBAC5C,CAAC,CAAC;YACL,CAAC;YACD,OAAO;QACT,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC;gBACjD,KAAK,EAAE;oBACL,EAAE,EAAE,QAAQ;oBACZ,EAAE,EAAE;wBACF,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE;wBACvB,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE;qBAC7B;iBACF;gBACD,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;aACrB,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,SAAS,CAAC;oBAClB,IAAI,EAAE,aAAa;oBACnB,OAAO,EAAE,mCAAmC;iBAC7C,CAAC,CAAC;YACL,CAAC;YACD,OAAO;QACT,CAAC;QACD,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC;gBACtD,KAAK,EAAE;oBACL,EAAE,EAAE,QAAQ;oBACZ,EAAE,EAAE;wBACF,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE;wBACvB,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE;qBAC7B;iBACF;gBACD,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;aACrB,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,SAAS,CAAC;oBAClB,IAAI,EAAE,aAAa;oBACnB,OAAO,EAAE,qCAAqC;iBAC/C,CAAC,CAAC;YACL,CAAC;YACD,OAAO;QACT,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAc,EACd,KAA6D;IAE7D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,mBAAmB,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtE,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAc,EACd,KAA6D;IAE7D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,mBAAmB,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,KAA8C;IAE9C,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACpE,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACZ,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","debug_id":"6ab21e9e-024c-5347-9864-92a8edf76dbe"}
|
|
1
|
+
{"version":3,"file":"tag.js","sources":["services/tag.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Tag service — institution-scoped tag CRUD and assignment to taggable\n * targets (users, classes, rubrics, boundaries).\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport {\n attachTagToTarget,\n createTag,\n deleteTag,\n detachTagFromTarget,\n findTagByName,\n getTag,\n listTagsAttachedTo,\n listTagsForSchool,\n type TagTarget,\n updateTag,\n} from \"../models/tag.js\";\nimport {\n findActiveSchoolAdminMembership,\n findSchoolMembership,\n} from \"../models/school.js\";\n\n/**\n * Normalises a tag name to a canonical display form: trimmed with internal\n * whitespace collapsed to single spaces. Casing is preserved so labels like\n * \"AP\" or \"Year 12\" read naturally; uniqueness is enforced case-insensitively\n * separately via {@link findTagByName}.\n */\nfunction normalizeTagName(name: string): string {\n const normalized = name.trim().replace(/\\s+/g, \" \");\n if (!normalized) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Tag name can't be empty\",\n });\n }\n return normalized;\n}\n\nasync function requireAdmin(schoolId: string, userId: string) {\n const membership = await findActiveSchoolAdminMembership(schoolId, userId);\n if (!membership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only school admins can manage tags\",\n });\n }\n}\n\nasync function requireMember(schoolId: string, userId: string) {\n const membership = await findSchoolMembership(schoolId, userId);\n if (!membership || membership.status !== \"ACTIVE\") {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You aren't a member of this institution\",\n });\n }\n return membership;\n}\n\nexport async function listTags(\n userId: string,\n input: { schoolId: string; departmentId?: string | null }\n) {\n await requireMember(input.schoolId, userId);\n return listTagsForSchool(input.schoolId, {\n departmentId: input.departmentId,\n });\n}\n\nexport async function createTagRecord(\n userId: string,\n input: {\n schoolId: string;\n departmentId?: string | null;\n name: string;\n color?: string;\n }\n) {\n await requireAdmin(input.schoolId, userId);\n if (input.departmentId) {\n const dept = await prisma.department.findUnique({\n where: { id: input.departmentId },\n select: { schoolId: true },\n });\n if (!dept || dept.schoolId !== input.schoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Department does not belong to this institution\",\n });\n }\n }\n const name = normalizeTagName(input.name);\n const existing = await findTagByName(input.schoolId, name);\n if (existing) {\n throw new TRPCError({\n code: \"CONFLICT\",\n message: `A tag named \"${existing.name}\" already exists in this institution`,\n });\n }\n return createTag({ ...input, name });\n}\n\nexport async function updateTagRecord(\n userId: string,\n input: {\n tagId: string;\n name?: string;\n color?: string;\n departmentId?: string | null;\n }\n) {\n const tag = await getTag(input.tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireAdmin(tag.schoolId, userId);\n if (input.departmentId) {\n const dept = await prisma.department.findUnique({\n where: { id: input.departmentId },\n select: { schoolId: true },\n });\n if (!dept || dept.schoolId !== tag.schoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Department does not belong to this institution\",\n });\n }\n }\n let name = input.name;\n if (name !== undefined) {\n name = normalizeTagName(name);\n const existing = await findTagByName(tag.schoolId, name, input.tagId);\n if (existing) {\n throw new TRPCError({\n code: \"CONFLICT\",\n message: `A tag named \"${existing.name}\" already exists in this institution`,\n });\n }\n }\n return updateTag(input.tagId, {\n name,\n color: input.color,\n departmentId: input.departmentId,\n });\n}\n\nexport async function deleteTagRecord(userId: string, tagId: string) {\n const tag = await getTag(tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireAdmin(tag.schoolId, userId);\n await deleteTag(tagId);\n return { success: true };\n}\n\nasync function ensureTargetInScope(\n schoolId: string,\n target: TagTarget,\n targetId: string\n) {\n switch (target) {\n case \"USER\": {\n const inSchool = await prisma.schoolMembership.findFirst({\n where: { schoolId, userId: targetId, status: \"ACTIVE\" },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"User is not in this institution\",\n });\n }\n return;\n }\n case \"CLASS\": {\n const inSchool = await prisma.class.findFirst({\n where: { id: targetId, schoolId },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Class is not in this institution\",\n });\n }\n return;\n }\n case \"RUBRIC\": {\n const inSchool = await prisma.markScheme.findFirst({\n where: {\n id: targetId,\n OR: [\n { class: { schoolId } },\n { department: { schoolId } },\n ],\n },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Rubric is not in this institution\",\n });\n }\n return;\n }\n case \"BOUNDARY\": {\n const inSchool = await prisma.gradingBoundary.findFirst({\n where: {\n id: targetId,\n OR: [\n { class: { schoolId } },\n { department: { schoolId } },\n ],\n },\n select: { id: true },\n });\n if (!inSchool) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Boundary is not in this institution\",\n });\n }\n return;\n }\n }\n}\n\nexport async function attachTag(\n userId: string,\n input: { tagId: string; target: TagTarget; targetId: string }\n) {\n const tag = await getTag(input.tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireMember(tag.schoolId, userId);\n await ensureTargetInScope(tag.schoolId, input.target, input.targetId);\n return attachTagToTarget(input);\n}\n\nexport async function detachTag(\n userId: string,\n input: { tagId: string; target: TagTarget; targetId: string }\n) {\n const tag = await getTag(input.tagId);\n if (!tag) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Tag not found\" });\n }\n await requireMember(tag.schoolId, userId);\n return detachTagFromTarget(input);\n}\n\nexport async function getAttachedTags(\n userId: string,\n input: { target: TagTarget; targetId: string }\n) {\n const tags = await listTagsAttachedTo(input.target, input.targetId);\n if (tags[0]) {\n await requireMember(tags[0].schoolId, userId);\n }\n return tags;\n}\n"],"names":[],"mappings":"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","debug_id":"b6385fba-13fc-519e-9095-49126706f392"}
|
package/package.json
CHANGED
package/src/models/tag.ts
CHANGED
|
@@ -41,6 +41,27 @@ export function getTag(tagId: string) {
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Finds a tag in the same school whose name matches case-insensitively, used to
|
|
46
|
+
* keep tag names normalised (no "Honors" / "honors" duplicates). Pass
|
|
47
|
+
* `excludeTagId` when checking during an update so the tag doesn't clash with
|
|
48
|
+
* itself.
|
|
49
|
+
*/
|
|
50
|
+
export function findTagByName(
|
|
51
|
+
schoolId: string,
|
|
52
|
+
name: string,
|
|
53
|
+
excludeTagId?: string
|
|
54
|
+
) {
|
|
55
|
+
return prisma.tag.findFirst({
|
|
56
|
+
where: {
|
|
57
|
+
schoolId,
|
|
58
|
+
name: { equals: name, mode: "insensitive" },
|
|
59
|
+
...(excludeTagId ? { id: { not: excludeTagId } } : {}),
|
|
60
|
+
},
|
|
61
|
+
select: { id: true, name: true },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
export function createTag(data: {
|
|
45
66
|
schoolId: string;
|
|
46
67
|
departmentId?: string | null;
|
package/src/services/tag.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createTag,
|
|
10
10
|
deleteTag,
|
|
11
11
|
detachTagFromTarget,
|
|
12
|
+
findTagByName,
|
|
12
13
|
getTag,
|
|
13
14
|
listTagsAttachedTo,
|
|
14
15
|
listTagsForSchool,
|
|
@@ -20,6 +21,23 @@ import {
|
|
|
20
21
|
findSchoolMembership,
|
|
21
22
|
} from "../models/school.js";
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Normalises a tag name to a canonical display form: trimmed with internal
|
|
26
|
+
* whitespace collapsed to single spaces. Casing is preserved so labels like
|
|
27
|
+
* "AP" or "Year 12" read naturally; uniqueness is enforced case-insensitively
|
|
28
|
+
* separately via {@link findTagByName}.
|
|
29
|
+
*/
|
|
30
|
+
function normalizeTagName(name: string): string {
|
|
31
|
+
const normalized = name.trim().replace(/\s+/g, " ");
|
|
32
|
+
if (!normalized) {
|
|
33
|
+
throw new TRPCError({
|
|
34
|
+
code: "BAD_REQUEST",
|
|
35
|
+
message: "Tag name can't be empty",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
async function requireAdmin(schoolId: string, userId: string) {
|
|
24
42
|
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
25
43
|
if (!membership) {
|
|
@@ -73,7 +91,15 @@ export async function createTagRecord(
|
|
|
73
91
|
});
|
|
74
92
|
}
|
|
75
93
|
}
|
|
76
|
-
|
|
94
|
+
const name = normalizeTagName(input.name);
|
|
95
|
+
const existing = await findTagByName(input.schoolId, name);
|
|
96
|
+
if (existing) {
|
|
97
|
+
throw new TRPCError({
|
|
98
|
+
code: "CONFLICT",
|
|
99
|
+
message: `A tag named "${existing.name}" already exists in this institution`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return createTag({ ...input, name });
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
export async function updateTagRecord(
|
|
@@ -102,8 +128,19 @@ export async function updateTagRecord(
|
|
|
102
128
|
});
|
|
103
129
|
}
|
|
104
130
|
}
|
|
131
|
+
let name = input.name;
|
|
132
|
+
if (name !== undefined) {
|
|
133
|
+
name = normalizeTagName(name);
|
|
134
|
+
const existing = await findTagByName(tag.schoolId, name, input.tagId);
|
|
135
|
+
if (existing) {
|
|
136
|
+
throw new TRPCError({
|
|
137
|
+
code: "CONFLICT",
|
|
138
|
+
message: `A tag named "${existing.name}" already exists in this institution`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
105
142
|
return updateTag(input.tagId, {
|
|
106
|
-
name
|
|
143
|
+
name,
|
|
107
144
|
color: input.color,
|
|
108
145
|
departmentId: input.departmentId,
|
|
109
146
|
});
|