@playcademy/vite-plugin 1.2.1-beta.24 → 1.2.1-beta.26
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/index.js +1053 -635
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -24315,7 +24315,7 @@ import path2 from "node:path";
|
|
|
24315
24315
|
// package.json
|
|
24316
24316
|
var package_default = {
|
|
24317
24317
|
name: "@playcademy/vite-plugin",
|
|
24318
|
-
version: "1.2.1-beta.
|
|
24318
|
+
version: "1.2.1-beta.26",
|
|
24319
24319
|
type: "module",
|
|
24320
24320
|
exports: {
|
|
24321
24321
|
".": {
|
|
@@ -24767,6 +24767,7 @@ import { stdout } from "process";
|
|
|
24767
24767
|
import { createHash } from "node:crypto";
|
|
24768
24768
|
import { createHash as createHash2 } from "node:crypto";
|
|
24769
24769
|
import crypto3 from "node:crypto";
|
|
24770
|
+
import * as crypto4 from "node:crypto";
|
|
24770
24771
|
import * as s3 from "fs";
|
|
24771
24772
|
import * as o3 from "path";
|
|
24772
24773
|
import fs22 from "node:fs";
|
|
@@ -24774,10 +24775,9 @@ import { dirname, isAbsolute, join as join4 } from "node:path";
|
|
|
24774
24775
|
import process22 from "process";
|
|
24775
24776
|
import os2 from "os";
|
|
24776
24777
|
import tty from "tty";
|
|
24777
|
-
import { randomUUID } from "crypto";
|
|
24778
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
24778
24779
|
import fs3 from "node:fs";
|
|
24779
24780
|
import fs4 from "node:fs";
|
|
24780
|
-
import * as crypto4 from "node:crypto";
|
|
24781
24781
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
24782
24782
|
var __create2 = Object.create;
|
|
24783
24783
|
var __getProtoOf2 = Object.getPrototypeOf;
|
|
@@ -25921,7 +25921,7 @@ var package_default2;
|
|
|
25921
25921
|
var init_package = __esm(() => {
|
|
25922
25922
|
package_default2 = {
|
|
25923
25923
|
name: "@playcademy/sandbox",
|
|
25924
|
-
version: "0.7.1-beta.
|
|
25924
|
+
version: "0.7.1-beta.24",
|
|
25925
25925
|
description: "Local development server for Playcademy game development",
|
|
25926
25926
|
type: "module",
|
|
25927
25927
|
exports: {
|
|
@@ -34376,6 +34376,9 @@ function isAssessmentAttemptOpen(attempt) {
|
|
|
34376
34376
|
function isAssessmentAttemptCompleted(attempt) {
|
|
34377
34377
|
return attempt.inProgress === ASSESSMENT_ATTEMPT_COMPLETED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_COMPLETED.scoreStatus;
|
|
34378
34378
|
}
|
|
34379
|
+
function isAssessmentAttemptSuperseded(attempt) {
|
|
34380
|
+
return attempt.inProgress === ASSESSMENT_ATTEMPT_SUPERSEDED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_SUPERSEDED.scoreStatus;
|
|
34381
|
+
}
|
|
34379
34382
|
function classifyAssessmentSubmission(attempt, submissionId) {
|
|
34380
34383
|
if (isAssessmentAttemptCompleted(attempt)) {
|
|
34381
34384
|
return attempt.submissionId === submissionId ? "replay" : "reject";
|
|
@@ -34433,6 +34436,15 @@ function assessmentReviewBankShortage(fulfillment) {
|
|
|
34433
34436
|
function assessmentFlowForPurpose(purpose) {
|
|
34434
34437
|
return purpose === "review" ? "item-submit" : "attempt-submit";
|
|
34435
34438
|
}
|
|
34439
|
+
function compareCodeUnits(left, right) {
|
|
34440
|
+
if (left < right) {
|
|
34441
|
+
return -1;
|
|
34442
|
+
}
|
|
34443
|
+
if (left > right) {
|
|
34444
|
+
return 1;
|
|
34445
|
+
}
|
|
34446
|
+
return 0;
|
|
34447
|
+
}
|
|
34436
34448
|
function canonicalJson(value) {
|
|
34437
34449
|
if (value === null || typeof value !== "object") {
|
|
34438
34450
|
return JSON.stringify(value) ?? "null";
|
|
@@ -34440,18 +34452,9 @@ function canonicalJson(value) {
|
|
|
34440
34452
|
if (Array.isArray(value)) {
|
|
34441
34453
|
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
34442
34454
|
}
|
|
34443
|
-
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) =>
|
|
34455
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
|
|
34444
34456
|
return `{${entries.join(",")}}`;
|
|
34445
34457
|
}
|
|
34446
|
-
function compareCanonicalKeys(left, right) {
|
|
34447
|
-
if (left < right) {
|
|
34448
|
-
return -1;
|
|
34449
|
-
}
|
|
34450
|
-
if (left > right) {
|
|
34451
|
-
return 1;
|
|
34452
|
-
}
|
|
34453
|
-
return 0;
|
|
34454
|
-
}
|
|
34455
34458
|
async function diagnosticRoutingRevision(manifest) {
|
|
34456
34459
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(manifest)));
|
|
34457
34460
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -35751,15 +35754,6 @@ function assessmentPresentationForAttempt(assessment, attemptId) {
|
|
|
35751
35754
|
});
|
|
35752
35755
|
return { ...assessment, items };
|
|
35753
35756
|
}
|
|
35754
|
-
function compareCodeUnits(left, right) {
|
|
35755
|
-
if (left < right) {
|
|
35756
|
-
return -1;
|
|
35757
|
-
}
|
|
35758
|
-
if (left > right) {
|
|
35759
|
-
return 1;
|
|
35760
|
-
}
|
|
35761
|
-
return 0;
|
|
35762
|
-
}
|
|
35763
35757
|
function reviewStandardFieldsWithinLimits(input) {
|
|
35764
35758
|
return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
|
|
35765
35759
|
}
|
|
@@ -35770,6 +35764,19 @@ function canonicalReviewStandardRef(input) {
|
|
|
35770
35764
|
}
|
|
35771
35765
|
return standard.framework && standard.identifier ? standard : null;
|
|
35772
35766
|
}
|
|
35767
|
+
function isKnownDescriptiveCurriculumFramework(framework) {
|
|
35768
|
+
return framework === "CCSS" || framework.startsWith("CCSS.");
|
|
35769
|
+
}
|
|
35770
|
+
function reviewRoutingStandardRefsFromMetadata(metadata2, options = {}) {
|
|
35771
|
+
const refs = new Map;
|
|
35772
|
+
for (const authored of qtiAssessmentStandardRefsFromMetadata(metadata2)) {
|
|
35773
|
+
const standard = canonicalReviewStandardRef(authored);
|
|
35774
|
+
if (standard && (!options.customOnly || !isKnownDescriptiveCurriculumFramework(standard.framework))) {
|
|
35775
|
+
refs.set(assessmentStandardRefKey(standard), standard);
|
|
35776
|
+
}
|
|
35777
|
+
}
|
|
35778
|
+
return [...refs.values()];
|
|
35779
|
+
}
|
|
35773
35780
|
function canonicalRequestStandards(standards) {
|
|
35774
35781
|
if (standards.length > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards) {
|
|
35775
35782
|
throw new RangeError(`A review request may contain at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards} standards`);
|
|
@@ -35825,42 +35832,172 @@ function qtiQuestionByIdentifier(questions) {
|
|
|
35825
35832
|
}
|
|
35826
35833
|
return byIdentifier;
|
|
35827
35834
|
}
|
|
35828
|
-
async function
|
|
35835
|
+
async function reviewBankRevision(input) {
|
|
35836
|
+
const alignmentSignature = input.items.map((item) => ({
|
|
35837
|
+
itemIdentifier: item.itemIdentifier,
|
|
35838
|
+
standardKeys: item.standards.map(assessmentStandardRefKey)
|
|
35839
|
+
}));
|
|
35840
|
+
const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
|
|
35841
|
+
bankIdentifier: input.bankIdentifier,
|
|
35842
|
+
contentRevision: input.sourceContentRevision,
|
|
35843
|
+
alignments: alignmentSignature
|
|
35844
|
+
})}`);
|
|
35845
|
+
return `review-bank-v1:${bankRevisionUuid}`;
|
|
35846
|
+
}
|
|
35847
|
+
async function reviewBankIndex(input) {
|
|
35848
|
+
return {
|
|
35849
|
+
...input,
|
|
35850
|
+
bankRevision: await reviewBankRevision(input)
|
|
35851
|
+
};
|
|
35852
|
+
}
|
|
35853
|
+
async function reviewBankSourceFingerprint(input) {
|
|
35854
|
+
const fingerprintUuid = await deterministicUUID(`playcademy:review-bank-source:v1:${JSON.stringify({
|
|
35855
|
+
bankIdentifier: input.bankIdentifier,
|
|
35856
|
+
items: input.items.map((item) => ({
|
|
35857
|
+
itemIdentifier: item.itemIdentifier,
|
|
35858
|
+
standardKeys: item.standards.map(assessmentStandardRefKey)
|
|
35859
|
+
}))
|
|
35860
|
+
})}`);
|
|
35861
|
+
return `review-bank-source-v1:${fingerprintUuid}`;
|
|
35862
|
+
}
|
|
35863
|
+
async function buildReviewBankIndex(assessment, questions, options = {}) {
|
|
35829
35864
|
if (assessment.identifier !== questions.assessmentTest) {
|
|
35830
35865
|
throw new Error(`QTI questions for ${questions.assessmentTest} do not describe assessment ${assessment.identifier}`);
|
|
35831
35866
|
}
|
|
35832
35867
|
const questionByIdentifier = qtiQuestionByIdentifier(questions);
|
|
35833
35868
|
const items = assessment.items.map((item) => {
|
|
35834
35869
|
const question = questionByIdentifier.get(item.identifier);
|
|
35835
|
-
const
|
|
35836
|
-
for (const authored of qtiAssessmentStandardRefsFromMetadata(question?.metadata)) {
|
|
35837
|
-
const standard = canonicalReviewStandardRef(authored);
|
|
35838
|
-
if (standard) {
|
|
35839
|
-
canonical.set(assessmentStandardRefKey(standard), standard);
|
|
35840
|
-
}
|
|
35841
|
-
}
|
|
35842
|
-
const standards = [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
35870
|
+
const standards = reviewRoutingStandardRefsFromMetadata(question?.metadata, options).toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
35843
35871
|
return {
|
|
35844
35872
|
itemIdentifier: item.identifier,
|
|
35845
35873
|
standards
|
|
35846
35874
|
};
|
|
35847
35875
|
});
|
|
35848
|
-
|
|
35849
|
-
itemIdentifier: item.itemIdentifier,
|
|
35850
|
-
standardKeys: item.standards.map(assessmentStandardRefKey)
|
|
35851
|
-
}));
|
|
35852
|
-
const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
|
|
35853
|
-
bankIdentifier: assessment.identifier,
|
|
35854
|
-
contentRevision: assessment.contentRevision,
|
|
35855
|
-
alignments: alignmentSignature
|
|
35856
|
-
})}`);
|
|
35857
|
-
return {
|
|
35876
|
+
return reviewBankIndex({
|
|
35858
35877
|
bankIdentifier: assessment.identifier,
|
|
35859
35878
|
sourceContentRevision: assessment.contentRevision,
|
|
35860
|
-
bankRevision: `review-bank-v1:${bankRevisionUuid}`,
|
|
35861
35879
|
items
|
|
35880
|
+
});
|
|
35881
|
+
}
|
|
35882
|
+
function emptyReviewBankManifest(bankIdentifier) {
|
|
35883
|
+
return {
|
|
35884
|
+
version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
|
|
35885
|
+
bankIdentifier,
|
|
35886
|
+
sourceFingerprint: null,
|
|
35887
|
+
sourceContentRevision: null,
|
|
35888
|
+
bankRevision: null,
|
|
35889
|
+
itemsByStandard: {}
|
|
35890
|
+
};
|
|
35891
|
+
}
|
|
35892
|
+
function reviewBankMappingIssues(bank) {
|
|
35893
|
+
return {
|
|
35894
|
+
missingOrInvalidItemIdentifiers: bank.items.filter((item) => item.standards.length === 0).map((item) => item.itemIdentifier),
|
|
35895
|
+
multipleStandardItemIdentifiers: bank.items.filter((item) => item.standards.length > 1).map((item) => item.itemIdentifier)
|
|
35896
|
+
};
|
|
35897
|
+
}
|
|
35898
|
+
async function buildReviewBankManifest(bank) {
|
|
35899
|
+
const itemsByStandard = new Map;
|
|
35900
|
+
for (const item of bank.items) {
|
|
35901
|
+
for (const standard of item.standards) {
|
|
35902
|
+
const key = assessmentStandardRefKey(standard);
|
|
35903
|
+
const identifiers = itemsByStandard.get(key) ?? [];
|
|
35904
|
+
identifiers.push(item.itemIdentifier);
|
|
35905
|
+
itemsByStandard.set(key, identifiers);
|
|
35906
|
+
}
|
|
35907
|
+
}
|
|
35908
|
+
return {
|
|
35909
|
+
version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
|
|
35910
|
+
bankIdentifier: bank.bankIdentifier,
|
|
35911
|
+
sourceFingerprint: await reviewBankSourceFingerprint(bank),
|
|
35912
|
+
sourceContentRevision: bank.sourceContentRevision,
|
|
35913
|
+
bankRevision: bank.bankRevision,
|
|
35914
|
+
itemsByStandard: Object.fromEntries([...itemsByStandard.entries()].toSorted(([left], [right]) => compareCodeUnits(left, right)))
|
|
35862
35915
|
};
|
|
35863
35916
|
}
|
|
35917
|
+
function requiredManifestString(record, field) {
|
|
35918
|
+
const value = record[field];
|
|
35919
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
35920
|
+
throw new Error(`Review-bank manifest requires ${field}`);
|
|
35921
|
+
}
|
|
35922
|
+
return value.trim();
|
|
35923
|
+
}
|
|
35924
|
+
function standardFromManifestKey(key) {
|
|
35925
|
+
try {
|
|
35926
|
+
const parsed = JSON.parse(key);
|
|
35927
|
+
if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") {
|
|
35928
|
+
return null;
|
|
35929
|
+
}
|
|
35930
|
+
const standard = canonicalReviewStandardRef({
|
|
35931
|
+
framework: parsed[0],
|
|
35932
|
+
identifier: parsed[1]
|
|
35933
|
+
});
|
|
35934
|
+
return standard && assessmentStandardRefKey(standard) === key ? standard : null;
|
|
35935
|
+
} catch {
|
|
35936
|
+
return null;
|
|
35937
|
+
}
|
|
35938
|
+
}
|
|
35939
|
+
async function reviewBankIndexFromManifest(metadata2, input) {
|
|
35940
|
+
const raw = isRecord(metadata2) ? metadata2[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY] : undefined;
|
|
35941
|
+
if (raw === undefined) {
|
|
35942
|
+
return null;
|
|
35943
|
+
}
|
|
35944
|
+
if (!isRecord(raw) || raw.version !== PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION) {
|
|
35945
|
+
throw new Error("Review-bank manifest has an unsupported version or shape");
|
|
35946
|
+
}
|
|
35947
|
+
const manifest = raw;
|
|
35948
|
+
const bankIdentifier = requiredManifestString(manifest, "bankIdentifier");
|
|
35949
|
+
if (bankIdentifier !== input.bankIdentifier) {
|
|
35950
|
+
throw new Error(`Review-bank manifest for ${bankIdentifier} does not describe ${input.bankIdentifier}`);
|
|
35951
|
+
}
|
|
35952
|
+
if (manifest.sourceFingerprint === null && manifest.sourceContentRevision === null && manifest.bankRevision === null) {
|
|
35953
|
+
return null;
|
|
35954
|
+
}
|
|
35955
|
+
const sourceFingerprint = requiredManifestString(manifest, "sourceFingerprint");
|
|
35956
|
+
const sourceContentRevision = requiredManifestString(manifest, "sourceContentRevision");
|
|
35957
|
+
const declaredBankRevision = requiredManifestString(manifest, "bankRevision");
|
|
35958
|
+
const itemsByStandard = manifest.itemsByStandard;
|
|
35959
|
+
if (!isRecord(itemsByStandard)) {
|
|
35960
|
+
throw new Error("Review-bank manifest requires an itemsByStandard mapping");
|
|
35961
|
+
}
|
|
35962
|
+
const membership = new Set(input.membershipItemIdentifiers);
|
|
35963
|
+
const standardsByItem = new Map;
|
|
35964
|
+
for (const [standardKey, rawItemIdentifiers] of Object.entries(itemsByStandard)) {
|
|
35965
|
+
const standard = standardFromManifestKey(standardKey);
|
|
35966
|
+
if (!standard || !Array.isArray(rawItemIdentifiers)) {
|
|
35967
|
+
throw new Error("Review-bank manifest contains an invalid standard mapping");
|
|
35968
|
+
}
|
|
35969
|
+
for (const rawItemIdentifier of rawItemIdentifiers) {
|
|
35970
|
+
if (typeof rawItemIdentifier !== "string" || !rawItemIdentifier.trim()) {
|
|
35971
|
+
throw new Error("Review-bank manifest contains an invalid item identifier");
|
|
35972
|
+
}
|
|
35973
|
+
const itemIdentifier = rawItemIdentifier.trim();
|
|
35974
|
+
if (membership.has(itemIdentifier)) {
|
|
35975
|
+
const standards = standardsByItem.get(itemIdentifier) ?? new Map;
|
|
35976
|
+
standards.set(assessmentStandardRefKey(standard), standard);
|
|
35977
|
+
standardsByItem.set(itemIdentifier, standards);
|
|
35978
|
+
}
|
|
35979
|
+
}
|
|
35980
|
+
}
|
|
35981
|
+
const index = await reviewBankIndex({
|
|
35982
|
+
bankIdentifier,
|
|
35983
|
+
sourceContentRevision,
|
|
35984
|
+
items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
|
|
35985
|
+
itemIdentifier,
|
|
35986
|
+
standards: [...standardsByItem.get(itemIdentifier)?.values() ?? []].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)))
|
|
35987
|
+
}))
|
|
35988
|
+
});
|
|
35989
|
+
const issues = reviewBankMappingIssues(index);
|
|
35990
|
+
if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
|
|
35991
|
+
throw new Error("Review-bank manifest must map every member to exactly one standard/node key");
|
|
35992
|
+
}
|
|
35993
|
+
if (await reviewBankSourceFingerprint(index) !== sourceFingerprint) {
|
|
35994
|
+
throw new Error("Review-bank manifest source fingerprint is stale");
|
|
35995
|
+
}
|
|
35996
|
+
if (index.bankRevision !== declaredBankRevision) {
|
|
35997
|
+
throw new Error("Review-bank manifest revision does not match its candidate index");
|
|
35998
|
+
}
|
|
35999
|
+
return index;
|
|
36000
|
+
}
|
|
35864
36001
|
function exposureTimestamp(value) {
|
|
35865
36002
|
const timestamp = Date.parse(value);
|
|
35866
36003
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
@@ -36348,6 +36485,7 @@ var COMMON_CORE_ELA_FRAMEWORK_ALIASES;
|
|
|
36348
36485
|
var COMMON_CORE_FRAMEWORK_ALIASES;
|
|
36349
36486
|
var ASSESSMENT_ATTEMPT_OPEN;
|
|
36350
36487
|
var ASSESSMENT_ATTEMPT_COMPLETED;
|
|
36488
|
+
var ASSESSMENT_ATTEMPT_SUPERSEDED;
|
|
36351
36489
|
var ASSESSMENT_RUNTIME_ERROR_STATUS;
|
|
36352
36490
|
var ROUTING_KEY_MAX_LENGTH = 128;
|
|
36353
36491
|
var ROUTING_RESULT_KEY_MAX_LENGTH = 256;
|
|
@@ -36367,6 +36505,8 @@ var DiagnosticRoutingManifestV1Schema;
|
|
|
36367
36505
|
var GRADE_VALUES;
|
|
36368
36506
|
var POINT_RESPONSE_PATTERN;
|
|
36369
36507
|
var REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1";
|
|
36508
|
+
var PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank";
|
|
36509
|
+
var PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1;
|
|
36370
36510
|
var DEFAULT_REVIEW_SELECTION_POLICY;
|
|
36371
36511
|
var RuntimeSubjectSchema;
|
|
36372
36512
|
var RuntimeGradeSchema;
|
|
@@ -36571,6 +36711,10 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36571
36711
|
inProgress: "false",
|
|
36572
36712
|
scoreStatus: "fully graded"
|
|
36573
36713
|
};
|
|
36714
|
+
ASSESSMENT_ATTEMPT_SUPERSEDED = {
|
|
36715
|
+
inProgress: "false",
|
|
36716
|
+
scoreStatus: "not submitted"
|
|
36717
|
+
};
|
|
36574
36718
|
ASSESSMENT_RUNTIME_ERROR_STATUS = {
|
|
36575
36719
|
NO_ELIGIBLE_TESTS: 404,
|
|
36576
36720
|
REVIEW_BANK_SHORTAGE: 422,
|
|
@@ -62910,67 +63054,6 @@ var init_infra2 = __esm(() => {
|
|
|
62910
63054
|
init_alerts_service();
|
|
62911
63055
|
init_kv_backup_service();
|
|
62912
63056
|
});
|
|
62913
|
-
function createInMemoryDatabaseLock() {
|
|
62914
|
-
const tails = new Map;
|
|
62915
|
-
return async (db2, key, operation) => {
|
|
62916
|
-
const previous = tails.get(key) ?? Promise.resolve();
|
|
62917
|
-
let release;
|
|
62918
|
-
const gate = new Promise((resolve2) => {
|
|
62919
|
-
release = resolve2;
|
|
62920
|
-
});
|
|
62921
|
-
const tail = previous.then(() => gate);
|
|
62922
|
-
tails.set(key, tail);
|
|
62923
|
-
await previous;
|
|
62924
|
-
try {
|
|
62925
|
-
return await operation(db2);
|
|
62926
|
-
} finally {
|
|
62927
|
-
release();
|
|
62928
|
-
if (tails.get(key) === tail) {
|
|
62929
|
-
tails.delete(key);
|
|
62930
|
-
}
|
|
62931
|
-
}
|
|
62932
|
-
};
|
|
62933
|
-
}
|
|
62934
|
-
var LockContentionError;
|
|
62935
|
-
var init_locks = __esm(() => {
|
|
62936
|
-
init_src6();
|
|
62937
|
-
LockContentionError = class LockContentionError2 extends Error {
|
|
62938
|
-
constructor(key) {
|
|
62939
|
-
super(`Lock is already held: ${key}`);
|
|
62940
|
-
this.name = "LockContentionError";
|
|
62941
|
-
}
|
|
62942
|
-
};
|
|
62943
|
-
});
|
|
62944
|
-
function assessmentRuntimeStartLockKey(input) {
|
|
62945
|
-
return JSON.stringify([
|
|
62946
|
-
ASSESSMENT_RUNTIME_LOCK_NAMESPACE,
|
|
62947
|
-
"start",
|
|
62948
|
-
input.studentId,
|
|
62949
|
-
input.activityId,
|
|
62950
|
-
input.purpose,
|
|
62951
|
-
input.courseId
|
|
62952
|
-
]);
|
|
62953
|
-
}
|
|
62954
|
-
function assessmentRuntimeAttemptLockKey(attemptId) {
|
|
62955
|
-
return JSON.stringify([ASSESSMENT_RUNTIME_LOCK_NAMESPACE, "attempt", attemptId]);
|
|
62956
|
-
}
|
|
62957
|
-
function createAssessmentRuntimeLock(lock) {
|
|
62958
|
-
return async (db2, key, operation) => {
|
|
62959
|
-
try {
|
|
62960
|
-
return await lock(db2, key, operation);
|
|
62961
|
-
} catch (error) {
|
|
62962
|
-
if (error instanceof LockContentionError) {
|
|
62963
|
-
throw new ServiceUnavailableError("Another assessment operation is in progress. Try again shortly.", { retryable: true });
|
|
62964
|
-
}
|
|
62965
|
-
throw error;
|
|
62966
|
-
}
|
|
62967
|
-
};
|
|
62968
|
-
}
|
|
62969
|
-
var ASSESSMENT_RUNTIME_LOCK_NAMESPACE = "playcademy-assessment-runtime-v1";
|
|
62970
|
-
var init_assessment_runtime_lock_util = __esm(() => {
|
|
62971
|
-
init_locks();
|
|
62972
|
-
init_errors2();
|
|
62973
|
-
});
|
|
62974
63057
|
function hasPagingOptions(options) {
|
|
62975
63058
|
return options.cursor !== undefined || options.limit !== undefined || options.delimiter !== undefined;
|
|
62976
63059
|
}
|
|
@@ -115025,73 +115108,6 @@ function stringField2(value) {
|
|
|
115025
115108
|
function firstStringField2(...values) {
|
|
115026
115109
|
return values.map(stringField2).find(Boolean) ?? "";
|
|
115027
115110
|
}
|
|
115028
|
-
function normalizedWhitespace2(value) {
|
|
115029
|
-
return value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
115030
|
-
}
|
|
115031
|
-
function normalizedIdentityCase2(value) {
|
|
115032
|
-
return value.toLocaleUpperCase("en-US");
|
|
115033
|
-
}
|
|
115034
|
-
function frameworkAliasKey2(value) {
|
|
115035
|
-
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
115036
|
-
}
|
|
115037
|
-
function isUuidShaped2(value) {
|
|
115038
|
-
return UUID_PATTERN2.test(value.trim());
|
|
115039
|
-
}
|
|
115040
|
-
function isCommonCoreMathIdentifier2(identifier) {
|
|
115041
|
-
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
115042
|
-
return canonical.startsWith("CCSS.MATH.") || /^(?:K|[1-8])\.(?:CC|OA|NBT|NF|MD|RP|NS|EE|F|G|SP)\./.test(canonical) || /^(?:HS)?(?:N|A|F|G|S)-[A-Z]+\./.test(canonical) || /^MP\.?\d/.test(canonical);
|
|
115043
|
-
}
|
|
115044
|
-
function isCommonCoreElaIdentifier2(identifier) {
|
|
115045
|
-
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
115046
|
-
return canonical.startsWith("CCSS.ELA-LITERACY.") || /^(?:RL|RI|RF|W|SL|L)\.(?:K|[1-9]|1[0-2])\./.test(canonical) || /^(?:RH|RST|WHST)\.(?:6-8|9-10|11-12)\./.test(canonical) || /^CCRA\.(?:R|W|SL|L)\./.test(canonical);
|
|
115047
|
-
}
|
|
115048
|
-
function canonicalFramework2(authoredFramework, identifier) {
|
|
115049
|
-
const aliasKey = frameworkAliasKey2(authoredFramework);
|
|
115050
|
-
if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
|
|
115051
|
-
return "CCSS.Math";
|
|
115052
|
-
}
|
|
115053
|
-
if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
|
|
115054
|
-
return "CCSS.ELA-Literacy";
|
|
115055
|
-
}
|
|
115056
|
-
if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
|
|
115057
|
-
return "CCSS";
|
|
115058
|
-
}
|
|
115059
|
-
return normalizedIdentityCase2(authoredFramework);
|
|
115060
|
-
}
|
|
115061
|
-
function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
115062
|
-
const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
|
|
115063
|
-
if (framework === "CCSS.Math") {
|
|
115064
|
-
return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
|
|
115065
|
-
}
|
|
115066
|
-
if (framework === "CCSS.ELA-Literacy") {
|
|
115067
|
-
return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
|
|
115068
|
-
}
|
|
115069
|
-
return identifier;
|
|
115070
|
-
}
|
|
115071
|
-
function readableAlignmentIdentifier2(alignment) {
|
|
115072
|
-
const identifier = alignment.identifier?.trim() ?? "";
|
|
115073
|
-
if (identifier && !isUuidShaped2(identifier)) {
|
|
115074
|
-
return identifier;
|
|
115075
|
-
}
|
|
115076
|
-
const id = alignment.id.trim();
|
|
115077
|
-
return id && !isUuidShaped2(id) ? id : "";
|
|
115078
|
-
}
|
|
115079
|
-
function canonicalAssessmentStandardRef2(standard) {
|
|
115080
|
-
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
115081
|
-
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
115082
|
-
const identifier = canonicalIdentifier2(framework, standard.identifier);
|
|
115083
|
-
return {
|
|
115084
|
-
framework,
|
|
115085
|
-
identifier
|
|
115086
|
-
};
|
|
115087
|
-
}
|
|
115088
|
-
function assessmentStandardRefKey2(standard) {
|
|
115089
|
-
const canonical = canonicalAssessmentStandardRef2(standard);
|
|
115090
|
-
return JSON.stringify([
|
|
115091
|
-
normalizedIdentityCase2(canonical.framework),
|
|
115092
|
-
normalizedIdentityCase2(canonical.identifier)
|
|
115093
|
-
]);
|
|
115094
|
-
}
|
|
115095
115111
|
function dedupeStandards2(standards) {
|
|
115096
115112
|
const deduped = new Map;
|
|
115097
115113
|
for (const standard of standards) {
|
|
@@ -115182,22 +115198,6 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
115182
115198
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
115183
115199
|
return dedupeStandards2(standards);
|
|
115184
115200
|
}
|
|
115185
|
-
function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
|
|
115186
|
-
const refs = new Map;
|
|
115187
|
-
for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
|
|
115188
|
-
const identifier = readableAlignmentIdentifier2(alignment);
|
|
115189
|
-
if (identifier) {
|
|
115190
|
-
const canonical = canonicalAssessmentStandardRef2({
|
|
115191
|
-
framework: alignment.source,
|
|
115192
|
-
identifier
|
|
115193
|
-
});
|
|
115194
|
-
if (canonical.framework && canonical.identifier) {
|
|
115195
|
-
refs.set(assessmentStandardRefKey2(canonical), canonical);
|
|
115196
|
-
}
|
|
115197
|
-
}
|
|
115198
|
-
}
|
|
115199
|
-
return [...refs.values()];
|
|
115200
|
-
}
|
|
115201
115201
|
var EVENT_HANDLER_ATTRIBUTE;
|
|
115202
115202
|
var SCRIPT_SCHEMES2;
|
|
115203
115203
|
var DATA_RASTER_IMAGE_PATTERN2;
|
|
@@ -115242,7 +115242,6 @@ var BLANK_SENTINEL22 = "
|
|
|
115242
115242
|
var PLAYABLE_POINT_VALUE;
|
|
115243
115243
|
var POINT_INTERACTION_TYPES2;
|
|
115244
115244
|
var INDEPENDENT_PROCESSING_TAGS2;
|
|
115245
|
-
var UUID_PATTERN2;
|
|
115246
115245
|
var COMMON_CORE_MATH_FRAMEWORK_ALIASES2;
|
|
115247
115246
|
var COMMON_CORE_ELA_FRAMEWORK_ALIASES2;
|
|
115248
115247
|
var COMMON_CORE_FRAMEWORK_ALIASES2;
|
|
@@ -115495,7 +115494,6 @@ var init_qti = __esm(() => {
|
|
|
115495
115494
|
"qti-sum",
|
|
115496
115495
|
"qti-base-value"
|
|
115497
115496
|
]);
|
|
115498
|
-
UUID_PATTERN2 = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
|
|
115499
115497
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
115500
115498
|
"ccssm",
|
|
115501
115499
|
"ccssmath",
|
|
@@ -117851,6 +117849,109 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
117851
117849
|
init_timeback_mastery_completion_util()
|
|
117852
117850
|
]);
|
|
117853
117851
|
});
|
|
117852
|
+
function createInMemoryDatabaseLock() {
|
|
117853
|
+
const tails = new Map;
|
|
117854
|
+
return async (db2, key, operation) => {
|
|
117855
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
117856
|
+
let release;
|
|
117857
|
+
const gate = new Promise((resolve2) => {
|
|
117858
|
+
release = resolve2;
|
|
117859
|
+
});
|
|
117860
|
+
const tail = previous.then(() => gate);
|
|
117861
|
+
tails.set(key, tail);
|
|
117862
|
+
await previous;
|
|
117863
|
+
try {
|
|
117864
|
+
return await operation(db2);
|
|
117865
|
+
} finally {
|
|
117866
|
+
release();
|
|
117867
|
+
if (tails.get(key) === tail) {
|
|
117868
|
+
tails.delete(key);
|
|
117869
|
+
}
|
|
117870
|
+
}
|
|
117871
|
+
};
|
|
117872
|
+
}
|
|
117873
|
+
var LockContentionError;
|
|
117874
|
+
var init_locks = __esm(() => {
|
|
117875
|
+
LockContentionError = class LockContentionError2 extends Error {
|
|
117876
|
+
constructor(key) {
|
|
117877
|
+
super(`Lock is already held: ${key}`);
|
|
117878
|
+
this.name = "LockContentionError";
|
|
117879
|
+
}
|
|
117880
|
+
};
|
|
117881
|
+
});
|
|
117882
|
+
function normalizeKeys(keys) {
|
|
117883
|
+
return [...new Set(keys)].toSorted();
|
|
117884
|
+
}
|
|
117885
|
+
function errorCode(error88) {
|
|
117886
|
+
if (typeof error88 !== "object" || error88 === null || !("code" in error88)) {
|
|
117887
|
+
return;
|
|
117888
|
+
}
|
|
117889
|
+
return typeof error88.code === "string" ? error88.code : undefined;
|
|
117890
|
+
}
|
|
117891
|
+
function translateAssessmentRuntimeLockError(error88) {
|
|
117892
|
+
if (error88 instanceof LockContentionError) {
|
|
117893
|
+
throw new ServiceUnavailableError("Another assessment operation is in progress. Try again shortly.", { retryable: true });
|
|
117894
|
+
}
|
|
117895
|
+
if (error88 instanceof AssessmentRuntimeLockTimeoutError) {
|
|
117896
|
+
throw new ServiceUnavailableError("The assessment operation took too long. Try again shortly.", { retryable: true, reason: "LOCK_TIMEOUT" });
|
|
117897
|
+
}
|
|
117898
|
+
const code = errorCode(error88);
|
|
117899
|
+
if (code && POSTGRES_TIMEOUT_ERROR_CODES.has(code)) {
|
|
117900
|
+
throw new ServiceUnavailableError("The assessment operation took too long. Try again shortly.", { retryable: true, reason: "LOCK_TIMEOUT" });
|
|
117901
|
+
}
|
|
117902
|
+
if (code && POSTGRES_CONNECTION_ERROR_CODES.has(code)) {
|
|
117903
|
+
throw new ServiceUnavailableError("The assessment lock connection was interrupted. Try again shortly.", { retryable: true, reason: "LOCK_CONNECTION_LOST" });
|
|
117904
|
+
}
|
|
117905
|
+
throw error88;
|
|
117906
|
+
}
|
|
117907
|
+
function createInMemoryAssessmentRuntimeLock() {
|
|
117908
|
+
const lock = createInMemoryDatabaseLock();
|
|
117909
|
+
return async (db2, keys, operation) => {
|
|
117910
|
+
const ordered = normalizeKeys(keys);
|
|
117911
|
+
function acquire(index2, operationDb) {
|
|
117912
|
+
const key = ordered[index2];
|
|
117913
|
+
return key ? lock(operationDb, key, (nested) => acquire(index2 + 1, nested)) : operation(operationDb);
|
|
117914
|
+
}
|
|
117915
|
+
try {
|
|
117916
|
+
return await acquire(0, db2);
|
|
117917
|
+
} catch (error88) {
|
|
117918
|
+
return translateAssessmentRuntimeLockError(error88);
|
|
117919
|
+
}
|
|
117920
|
+
};
|
|
117921
|
+
}
|
|
117922
|
+
function assessmentRuntimeStartLockKey(input) {
|
|
117923
|
+
return JSON.stringify([
|
|
117924
|
+
ASSESSMENT_RUNTIME_LOCK_NAMESPACE,
|
|
117925
|
+
"start",
|
|
117926
|
+
input.studentId,
|
|
117927
|
+
input.activityId,
|
|
117928
|
+
input.purpose,
|
|
117929
|
+
input.courseId
|
|
117930
|
+
]);
|
|
117931
|
+
}
|
|
117932
|
+
function assessmentRuntimeAttemptLockKey(attemptId) {
|
|
117933
|
+
return JSON.stringify([ASSESSMENT_RUNTIME_LOCK_NAMESPACE, "attempt", attemptId]);
|
|
117934
|
+
}
|
|
117935
|
+
var ASSESSMENT_RUNTIME_LOCK_NAMESPACE = "playcademy-assessment-runtime-v1";
|
|
117936
|
+
var POSTGRES_TIMEOUT_ERROR_CODES;
|
|
117937
|
+
var POSTGRES_CONNECTION_ERROR_CODES;
|
|
117938
|
+
var AssessmentRuntimeLockTimeoutError;
|
|
117939
|
+
var init_assessment_runtime_lock_util = __esm(() => {
|
|
117940
|
+
init_locks();
|
|
117941
|
+
init_errors2();
|
|
117942
|
+
POSTGRES_TIMEOUT_ERROR_CODES = new Set(["25P03", "57014"]);
|
|
117943
|
+
POSTGRES_CONNECTION_ERROR_CODES = new Set([
|
|
117944
|
+
"CONNECTION_CLOSED",
|
|
117945
|
+
"CONNECTION_DESTROYED",
|
|
117946
|
+
"CONNECTION_ENDED"
|
|
117947
|
+
]);
|
|
117948
|
+
AssessmentRuntimeLockTimeoutError = class AssessmentRuntimeLockTimeoutError2 extends Error {
|
|
117949
|
+
constructor(timeoutMs) {
|
|
117950
|
+
super(`Assessment operation exceeded its ${timeoutMs}ms lock deadline`);
|
|
117951
|
+
this.name = "AssessmentRuntimeLockTimeoutError";
|
|
117952
|
+
}
|
|
117953
|
+
};
|
|
117954
|
+
});
|
|
117854
117955
|
function validateAssessmentStatusTransition(current, next) {
|
|
117855
117956
|
if (current === next) {
|
|
117856
117957
|
return;
|
|
@@ -117895,8 +117996,8 @@ function assertAssessmentHasQuestions(questions) {
|
|
|
117895
117996
|
}
|
|
117896
117997
|
}
|
|
117897
117998
|
function assertReviewAssessmentHasStandards(standardCounts) {
|
|
117898
|
-
if (standardCounts.some((count) => count
|
|
117899
|
-
throw new ValidationError("Every question in a review assessment must have
|
|
117999
|
+
if (standardCounts.some((count) => count !== 1)) {
|
|
118000
|
+
throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
|
|
117900
118001
|
}
|
|
117901
118002
|
}
|
|
117902
118003
|
function planAssessmentRemoval(status) {
|
|
@@ -117959,6 +118060,41 @@ function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
|
117959
118060
|
var init_timeback_assessment_rules_util = __esm(() => {
|
|
117960
118061
|
init_errors2();
|
|
117961
118062
|
});
|
|
118063
|
+
function stageAssessmentAttemptSupersession(attempt) {
|
|
118064
|
+
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
118065
|
+
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
118066
|
+
}
|
|
118067
|
+
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
118068
|
+
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
118069
|
+
}
|
|
118070
|
+
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
118071
|
+
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
118072
|
+
inProgress: result.inProgress ?? "",
|
|
118073
|
+
scoreStatus: result.scoreStatus
|
|
118074
|
+
})) {
|
|
118075
|
+
return null;
|
|
118076
|
+
}
|
|
118077
|
+
return {
|
|
118078
|
+
status: "active",
|
|
118079
|
+
assessmentLineItem: result.assessmentLineItem,
|
|
118080
|
+
student: result.student,
|
|
118081
|
+
score: result.score ?? 0,
|
|
118082
|
+
scoreDate: result.scoreDate,
|
|
118083
|
+
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
118084
|
+
metadata: result.metadata ?? {}
|
|
118085
|
+
};
|
|
118086
|
+
}
|
|
118087
|
+
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
118088
|
+
let decision = initialDecision;
|
|
118089
|
+
while (decision?.kind === "resume") {
|
|
118090
|
+
const snapshot = await resume(decision);
|
|
118091
|
+
if (snapshot !== null) {
|
|
118092
|
+
return { kind: "resumed", snapshot };
|
|
118093
|
+
}
|
|
118094
|
+
decision = select3();
|
|
118095
|
+
}
|
|
118096
|
+
return { kind: "continue", decision };
|
|
118097
|
+
}
|
|
117962
118098
|
function buildReviewItemResultMetadata(input) {
|
|
117963
118099
|
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
117964
118100
|
if (!administration) {
|
|
@@ -118403,8 +118539,8 @@ function parsePlayableItem(question) {
|
|
|
118403
118539
|
function assessmentContentRevision(test, questions) {
|
|
118404
118540
|
return sha256Hex(canonicalJson2({
|
|
118405
118541
|
identifier: test.identifier,
|
|
118542
|
+
title: test.title,
|
|
118406
118543
|
qtiVersion: test.qtiVersion,
|
|
118407
|
-
rawXml: test.rawXml,
|
|
118408
118544
|
parts: test["qti-test-part"],
|
|
118409
118545
|
questions: questions.questions.map(({ reference, question }) => ({
|
|
118410
118546
|
identifier: reference.identifier,
|
|
@@ -118560,11 +118696,30 @@ async function hydrateQtiTestQuestions(client2, references) {
|
|
|
118560
118696
|
}));
|
|
118561
118697
|
return { ...references, questions };
|
|
118562
118698
|
}
|
|
118563
|
-
async function
|
|
118699
|
+
async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifiers) {
|
|
118700
|
+
const referenceByIdentifier = new Map(references.questions.map((reference) => [reference.reference.identifier, reference]));
|
|
118701
|
+
const selectedReferences = itemIdentifiers.map((itemIdentifier) => {
|
|
118702
|
+
const reference = referenceByIdentifier.get(itemIdentifier);
|
|
118703
|
+
if (!reference) {
|
|
118704
|
+
throw new Error(`QTI assessment ${references.assessmentTest} does not contain item ${itemIdentifier}`);
|
|
118705
|
+
}
|
|
118706
|
+
return reference;
|
|
118707
|
+
});
|
|
118708
|
+
return hydrateQtiTestQuestions(client2, {
|
|
118709
|
+
...references,
|
|
118710
|
+
totalQuestions: selectedReferences.length,
|
|
118711
|
+
questions: selectedReferences
|
|
118712
|
+
});
|
|
118713
|
+
}
|
|
118714
|
+
async function loadQtiTestReferences(client2, identifier) {
|
|
118564
118715
|
const [test, references] = await Promise.all([
|
|
118565
118716
|
client2.qtiApi.assessmentTests.get(identifier),
|
|
118566
118717
|
client2.qtiApi.assessmentTests.getQuestions(identifier)
|
|
118567
118718
|
]);
|
|
118719
|
+
return { test, references };
|
|
118720
|
+
}
|
|
118721
|
+
async function loadHydratedQtiTest(client2, identifier) {
|
|
118722
|
+
const { test, references } = await loadQtiTestReferences(client2, identifier);
|
|
118568
118723
|
return {
|
|
118569
118724
|
test,
|
|
118570
118725
|
questions: await hydrateQtiTestQuestions(client2, references)
|
|
@@ -118578,6 +118733,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
118578
118733
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
118579
118734
|
static EXPORT_CONCURRENCY = 4;
|
|
118580
118735
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
118736
|
+
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
118581
118737
|
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
118582
118738
|
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
118583
118739
|
static SCORING_CONCURRENCY = 4;
|
|
@@ -118590,6 +118746,10 @@ class TimebackAssessmentRuntimeService {
|
|
|
118590
118746
|
defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
|
|
118591
118747
|
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
118592
118748
|
});
|
|
118749
|
+
reviewSelectionCache = new TimebackCache({
|
|
118750
|
+
defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
|
|
118751
|
+
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
118752
|
+
});
|
|
118593
118753
|
constructor(deps) {
|
|
118594
118754
|
this.deps = deps;
|
|
118595
118755
|
}
|
|
@@ -118608,14 +118768,15 @@ class TimebackAssessmentRuntimeService {
|
|
|
118608
118768
|
}
|
|
118609
118769
|
const masteryStandard = input.purpose === "mastery" ? normalizeMasteryStandard(input.standard) : undefined;
|
|
118610
118770
|
const requestFingerprint = this.startRequestFingerprint(input);
|
|
118771
|
+
const pendingSupersessions = new Set;
|
|
118611
118772
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
118612
118773
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
118613
118774
|
studentId,
|
|
118614
118775
|
activityId: input.activityId,
|
|
118615
118776
|
purpose: input.purpose,
|
|
118616
118777
|
courseId: row.courseId
|
|
118617
|
-
}))
|
|
118618
|
-
return this.
|
|
118778
|
+
}));
|
|
118779
|
+
return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
|
|
118619
118780
|
const context2 = await this.selectIntegration(candidates, studentId, input);
|
|
118620
118781
|
const attemptScope = {
|
|
118621
118782
|
studentId,
|
|
@@ -118630,20 +118791,21 @@ class TimebackAssessmentRuntimeService {
|
|
|
118630
118791
|
this.listAttempts(attemptScope)
|
|
118631
118792
|
]);
|
|
118632
118793
|
const attempts = listing.attempts;
|
|
118633
|
-
const
|
|
118634
|
-
|
|
118635
|
-
|
|
118636
|
-
return
|
|
118794
|
+
const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, attempts, context2.integration));
|
|
118795
|
+
if (initialResolution.kind === "resumed") {
|
|
118796
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
118797
|
+
return initialResolution.snapshot;
|
|
118637
118798
|
}
|
|
118799
|
+
let decision = this.requireDecision(initialResolution.decision, input.purpose);
|
|
118638
118800
|
let assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
|
|
118639
118801
|
const refreshedListing = await this.listAttempts(attemptScope);
|
|
118640
118802
|
const refreshed = refreshedListing.attempts;
|
|
118641
|
-
const
|
|
118642
|
-
|
|
118643
|
-
|
|
118644
|
-
return
|
|
118803
|
+
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, refreshed, context2.integration));
|
|
118804
|
+
if (refreshedResolution.kind === "resumed") {
|
|
118805
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
118806
|
+
return refreshedResolution.snapshot;
|
|
118645
118807
|
}
|
|
118646
|
-
decision =
|
|
118808
|
+
decision = this.requireDecision(refreshedResolution.decision, input.purpose);
|
|
118647
118809
|
if (assessment.identifier !== decision.test.qtiTestIdentifier) {
|
|
118648
118810
|
assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
|
|
118649
118811
|
}
|
|
@@ -118701,6 +118863,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
118701
118863
|
grade: context2.integration.grade
|
|
118702
118864
|
})
|
|
118703
118865
|
});
|
|
118866
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
118704
118867
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
118705
118868
|
});
|
|
118706
118869
|
}
|
|
@@ -118710,14 +118873,15 @@ class TimebackAssessmentRuntimeService {
|
|
|
118710
118873
|
input
|
|
118711
118874
|
}) {
|
|
118712
118875
|
const requestFingerprint = this.startRequestFingerprint(input);
|
|
118876
|
+
const pendingSupersessions = new Set;
|
|
118713
118877
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
118714
118878
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
118715
118879
|
studentId,
|
|
118716
118880
|
activityId: input.activityId,
|
|
118717
118881
|
purpose: input.purpose,
|
|
118718
118882
|
courseId: row.courseId
|
|
118719
|
-
}))
|
|
118720
|
-
return this.
|
|
118883
|
+
}));
|
|
118884
|
+
return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
|
|
118721
118885
|
const context2 = await this.selectIntegration(candidates, studentId, input);
|
|
118722
118886
|
const attemptScope = {
|
|
118723
118887
|
studentId,
|
|
@@ -118731,22 +118895,23 @@ class TimebackAssessmentRuntimeService {
|
|
|
118731
118895
|
this.liveTests(context2.integration.id, input.purpose, db2, undefined, input.diagnosticKey),
|
|
118732
118896
|
this.listAttempts(attemptScope)
|
|
118733
118897
|
]);
|
|
118734
|
-
const
|
|
118735
|
-
|
|
118736
|
-
|
|
118737
|
-
return
|
|
118898
|
+
const initialResolution = await this.resumeAvailableAttempt(tests, listing.attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, listing.attempts, context2.integration, db2));
|
|
118899
|
+
if (initialResolution.kind === "resumed") {
|
|
118900
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
118901
|
+
return initialResolution.snapshot;
|
|
118738
118902
|
}
|
|
118903
|
+
let decision = this.requireDecision(initialResolution.decision, input.purpose);
|
|
118739
118904
|
let definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
|
|
118740
118905
|
let assessment = await this.loadAssessment(definition.qtiTestIdentifier);
|
|
118741
118906
|
let routing = await this.initializeHostedDiagnostic(definition, assessment);
|
|
118742
118907
|
const refreshedListing = await this.listAttempts(attemptScope);
|
|
118743
118908
|
const refreshed = refreshedListing.attempts;
|
|
118744
|
-
const
|
|
118745
|
-
|
|
118746
|
-
|
|
118747
|
-
return
|
|
118909
|
+
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, refreshed, context2.integration, db2));
|
|
118910
|
+
if (refreshedResolution.kind === "resumed") {
|
|
118911
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
118912
|
+
return refreshedResolution.snapshot;
|
|
118748
118913
|
}
|
|
118749
|
-
decision =
|
|
118914
|
+
decision = this.requireDecision(refreshedResolution.decision, input.purpose);
|
|
118750
118915
|
if (definition.id !== decision.test.id) {
|
|
118751
118916
|
definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
|
|
118752
118917
|
assessment = await this.loadAssessment(definition.qtiTestIdentifier);
|
|
@@ -118815,6 +118980,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
118815
118980
|
grade: context2.integration.grade
|
|
118816
118981
|
})
|
|
118817
118982
|
});
|
|
118983
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
118818
118984
|
return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, routing.state);
|
|
118819
118985
|
});
|
|
118820
118986
|
}
|
|
@@ -118823,7 +118989,9 @@ class TimebackAssessmentRuntimeService {
|
|
|
118823
118989
|
studentId,
|
|
118824
118990
|
input
|
|
118825
118991
|
}) {
|
|
118992
|
+
const preparationStartedAt = Date.now();
|
|
118826
118993
|
const requestFingerprint = reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
|
|
118994
|
+
const pendingSupersessions = new Set;
|
|
118827
118995
|
const normalizedRequest = normalizeReviewRequest(input);
|
|
118828
118996
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
118829
118997
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
@@ -118831,8 +118999,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
118831
118999
|
activityId: input.activityId,
|
|
118832
119000
|
purpose: input.purpose,
|
|
118833
119001
|
courseId: row.courseId
|
|
118834
|
-
}))
|
|
118835
|
-
return this.
|
|
119002
|
+
}));
|
|
119003
|
+
return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
|
|
118836
119004
|
const context2 = await this.selectIntegration(candidates, studentId, input);
|
|
118837
119005
|
const attemptScope = {
|
|
118838
119006
|
studentId,
|
|
@@ -118847,38 +119015,44 @@ class TimebackAssessmentRuntimeService {
|
|
|
118847
119015
|
this.listAttempts(attemptScope)
|
|
118848
119016
|
]);
|
|
118849
119017
|
const attempts = listing.attempts;
|
|
118850
|
-
const
|
|
118851
|
-
|
|
118852
|
-
|
|
118853
|
-
return
|
|
119018
|
+
const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, listing.reviewChildren, context2));
|
|
119019
|
+
if (initialResolution.kind === "resumed") {
|
|
119020
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
119021
|
+
return initialResolution.snapshot;
|
|
118854
119022
|
}
|
|
119023
|
+
let decision = initialResolution.decision;
|
|
118855
119024
|
if (tests.length === 0 || !decision) {
|
|
118856
119025
|
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
|
|
118857
119026
|
}
|
|
118858
119027
|
if (tests.length > 1) {
|
|
118859
119028
|
throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
|
|
118860
119029
|
}
|
|
118861
|
-
|
|
119030
|
+
const manifestStartedAt = Date.now();
|
|
119031
|
+
let catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
119032
|
+
this.recordReviewPreparationPhase("manifest_load", manifestStartedAt, {
|
|
119033
|
+
candidateCount: catalog.bank.items.length
|
|
119034
|
+
});
|
|
118862
119035
|
const refreshedListing = await this.listAttempts(attemptScope);
|
|
118863
119036
|
const refreshed = refreshedListing.attempts;
|
|
118864
|
-
const
|
|
118865
|
-
|
|
118866
|
-
|
|
118867
|
-
return
|
|
119037
|
+
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, refreshedListing.reviewChildren, context2));
|
|
119038
|
+
if (refreshedResolution.kind === "resumed") {
|
|
119039
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
119040
|
+
return refreshedResolution.snapshot;
|
|
118868
119041
|
}
|
|
119042
|
+
const refreshedDecision = refreshedResolution.decision;
|
|
118869
119043
|
if (!refreshedDecision || refreshedDecision.kind !== "start") {
|
|
118870
119044
|
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
|
|
118871
119045
|
}
|
|
118872
119046
|
decision = refreshedDecision;
|
|
118873
|
-
if (
|
|
118874
|
-
|
|
119047
|
+
if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
|
|
119048
|
+
catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
118875
119049
|
}
|
|
118876
119050
|
const exposures = this.reviewExposures(refreshedListing.reviewChildren.values(), {
|
|
118877
|
-
bankRevision:
|
|
119051
|
+
bankRevision: catalog.bank.bankRevision
|
|
118878
119052
|
});
|
|
118879
119053
|
const selected = selectReviewItems({
|
|
118880
119054
|
request: input,
|
|
118881
|
-
bank:
|
|
119055
|
+
bank: catalog.bank,
|
|
118882
119056
|
exposures
|
|
118883
119057
|
});
|
|
118884
119058
|
if (selected.selections.length === 0) {
|
|
@@ -118887,7 +119061,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
118887
119061
|
}
|
|
118888
119062
|
throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
|
|
118889
119063
|
}
|
|
119064
|
+
const hydrationStartedAt = Date.now();
|
|
119065
|
+
const source = await this.hydrateReviewBankSelection(catalog, selected.selections.map((selection) => selection.itemIdentifier));
|
|
119066
|
+
this.recordReviewPreparationPhase("selected_hydration", hydrationStartedAt, {
|
|
119067
|
+
selectedCount: selected.selections.length
|
|
119068
|
+
});
|
|
118890
119069
|
const assessment = projectReviewAssessment(source.assessment, source.bank, selected.selections);
|
|
119070
|
+
const provisioningStartedAt = Date.now();
|
|
118891
119071
|
const parentLineItemId = await this.ensureReviewBankLineItem({
|
|
118892
119072
|
integration: context2.integration,
|
|
118893
119073
|
activityId: input.activityId,
|
|
@@ -118950,6 +119130,12 @@ class TimebackAssessmentRuntimeService {
|
|
|
118950
119130
|
existingChildResults: refreshedListing.reviewChildren,
|
|
118951
119131
|
lookupMissingResults: false
|
|
118952
119132
|
});
|
|
119133
|
+
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
119134
|
+
this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
|
|
119135
|
+
this.recordReviewPreparationPhase("total", preparationStartedAt, {
|
|
119136
|
+
candidateCount: catalog.bank.items.length,
|
|
119137
|
+
selectedCount: selected.selections.length
|
|
119138
|
+
});
|
|
118953
119139
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
118954
119140
|
});
|
|
118955
119141
|
}
|
|
@@ -119032,7 +119218,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
119032
119218
|
"app.error.message": errorMessage2(error88)
|
|
119033
119219
|
});
|
|
119034
119220
|
}
|
|
119035
|
-
return this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(params.attemptId), async (db2) => {
|
|
119221
|
+
return this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
|
|
119036
119222
|
const attempt = await this.authorizeAttempt(params, {
|
|
119037
119223
|
db: db2,
|
|
119038
119224
|
developerAccessValidated: true
|
|
@@ -119080,7 +119266,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
119080
119266
|
let preview = await this.prepareItemSubmission(params, input);
|
|
119081
119267
|
let preparationAttempts = 1;
|
|
119082
119268
|
while (true) {
|
|
119083
|
-
const committed = await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(params.attemptId), async (db2) => {
|
|
119269
|
+
const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
|
|
119084
119270
|
const attempt = await this.authorizeAttempt(params, {
|
|
119085
119271
|
db: db2,
|
|
119086
119272
|
developerAccessValidated: true
|
|
@@ -119176,7 +119362,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
119176
119362
|
let preview = await this.prepareDiagnosticItemSubmission(params, input);
|
|
119177
119363
|
let preparationAttempts = 1;
|
|
119178
119364
|
while (true) {
|
|
119179
|
-
const committed = await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(params.attemptId), async (db2) => {
|
|
119365
|
+
const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
|
|
119180
119366
|
const attempt = await this.authorizeAttempt(params, {
|
|
119181
119367
|
db: db2,
|
|
119182
119368
|
developerAccessValidated: true
|
|
@@ -119303,7 +119489,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
119303
119489
|
preparationAttempts += 1;
|
|
119304
119490
|
preview = await this.scoreDiagnosticItemSubmission(params, input);
|
|
119305
119491
|
} else {
|
|
119306
|
-
await this.projectDiagnosticItemResponse(committed.projection);
|
|
119492
|
+
await this.projectDiagnosticItemResponse(committed.projection, preview?.assessment);
|
|
119307
119493
|
return committed.response;
|
|
119308
119494
|
}
|
|
119309
119495
|
}
|
|
@@ -119510,7 +119696,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
119510
119696
|
}) {
|
|
119511
119697
|
await this.deps.validateDeveloperAccess(params.user, params.gameId);
|
|
119512
119698
|
const preview = await this.prepareSubmission(params, input);
|
|
119513
|
-
const submission = await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(params.attemptId), async (db2) => {
|
|
119699
|
+
const submission = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
|
|
119514
119700
|
const attempt = await this.authorizeAttempt(params, {
|
|
119515
119701
|
db: db2,
|
|
119516
119702
|
developerAccessValidated: true
|
|
@@ -119717,7 +119903,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
119717
119903
|
where: and(eq(gameTimebackAssessmentTests.id, test.id), eq(gameTimebackAssessmentTests.integrationId, integration.id))
|
|
119718
119904
|
}) : undefined;
|
|
119719
119905
|
const diagnostic = definition?.diagnosticKey ? await this.initializeHostedDiagnostic(definition, loaded.assessment) : null;
|
|
119720
|
-
const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions) : null;
|
|
119906
|
+
const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions, { customOnly: true }) : null;
|
|
119721
119907
|
return {
|
|
119722
119908
|
...test,
|
|
119723
119909
|
live: true,
|
|
@@ -119780,7 +119966,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
119780
119966
|
const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.course.id, enrollment]));
|
|
119781
119967
|
const candidateRows = integrations.filter((integration) => enrollmentByCourse.has(integration.courseId));
|
|
119782
119968
|
const liveTestRows = candidateRows.length === 0 ? [] : await db2.query.gameTimebackAssessmentTests.findMany({
|
|
119783
|
-
where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
119969
|
+
where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live")),
|
|
119970
|
+
columns: {
|
|
119971
|
+
integrationId: true,
|
|
119972
|
+
standardFramework: true,
|
|
119973
|
+
standardIdentifier: true,
|
|
119974
|
+
diagnosticKey: true
|
|
119975
|
+
}
|
|
119784
119976
|
});
|
|
119785
119977
|
const matchesMasteryStandard = input.purpose === "mastery" ? masteryStandardMatcher(input.standard) : undefined;
|
|
119786
119978
|
const integrationIdsWithLiveTests = new Set(liveTestRows.filter((row) => (!matchesMasteryStandard || matchesMasteryStandard(assessmentStandardForRow(row))) && (input.purpose !== "diagnostic" || row.diagnosticKey === input.diagnosticKey)).map((row) => row.integrationId));
|
|
@@ -119827,10 +120019,6 @@ class TimebackAssessmentRuntimeService {
|
|
|
119827
120019
|
enrollment: enrollmentByCourse.get(selection.selected.courseId)
|
|
119828
120020
|
};
|
|
119829
120021
|
}
|
|
119830
|
-
async withStartLocks(keys, operation) {
|
|
119831
|
-
const acquire = (index2, db2) => index2 === keys.length ? operation(db2) : this.deps.assessmentRuntimeLock(db2, keys[index2], (nested) => acquire(index2 + 1, nested));
|
|
119832
|
-
return acquire(0, this.deps.db);
|
|
119833
|
-
}
|
|
119834
120022
|
async liveTests(integrationId, purpose, db2 = this.deps.db, requestedStandard, requestedDiagnosticKey) {
|
|
119835
120023
|
const matchesMasteryStandard = requestedStandard ? masteryStandardMatcher(requestedStandard) : undefined;
|
|
119836
120024
|
const rows = await db2.query.gameTimebackAssessmentTests.findMany({
|
|
@@ -119973,6 +120161,75 @@ class TimebackAssessmentRuntimeService {
|
|
|
119973
120161
|
});
|
|
119974
120162
|
return this.diagnosticSnapshot(resumed.result, resumed.metadata, assessment, integration, routing.state);
|
|
119975
120163
|
}
|
|
120164
|
+
async resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, resume) {
|
|
120165
|
+
const select3 = () => selectRuntimeAssessment(tests, [...this.attemptsMatchingRequest(attempts, requestFingerprint).values()].map((entry2) => entry2.selection));
|
|
120166
|
+
return resolveAvailableAssessmentAttempt(select3(), select3, (decision) => this.resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, () => resume(decision)));
|
|
120167
|
+
}
|
|
120168
|
+
async resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, resume) {
|
|
120169
|
+
try {
|
|
120170
|
+
return await resume();
|
|
120171
|
+
} catch (error88) {
|
|
120172
|
+
if (!(error88 instanceof AssessmentRuntimeError) || error88.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
|
|
120173
|
+
throw error88;
|
|
120174
|
+
}
|
|
120175
|
+
if (shouldConfirmAssessmentVersionUnavailable(error88.details)) {
|
|
120176
|
+
await sleep(TimebackAssessmentRuntimeService.ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS);
|
|
120177
|
+
try {
|
|
120178
|
+
return await resume();
|
|
120179
|
+
} catch (confirmationError) {
|
|
120180
|
+
if (!(confirmationError instanceof AssessmentRuntimeError) || confirmationError.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
|
|
120181
|
+
throw confirmationError;
|
|
120182
|
+
}
|
|
120183
|
+
}
|
|
120184
|
+
}
|
|
120185
|
+
const staged = attempts.get(decision.attempt.attemptId);
|
|
120186
|
+
pendingSupersessions.add(staged.result.sourcedId);
|
|
120187
|
+
stageAssessmentAttemptSupersession(staged);
|
|
120188
|
+
addEvent("assessment.attempt_supersession_staged", {
|
|
120189
|
+
"app.assessment.attempt_id": decision.attempt.attemptId,
|
|
120190
|
+
"app.assessment.reason": "selected_test_version_unavailable"
|
|
120191
|
+
});
|
|
120192
|
+
return null;
|
|
120193
|
+
}
|
|
120194
|
+
}
|
|
120195
|
+
async persistPendingSupersessions(db2, attemptIds) {
|
|
120196
|
+
for (const attemptId of [...attemptIds].toSorted()) {
|
|
120197
|
+
await this.deps.assessmentRuntimeLock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
120198
|
+
let result;
|
|
120199
|
+
try {
|
|
120200
|
+
result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
|
|
120201
|
+
} catch (error88) {
|
|
120202
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
120203
|
+
return;
|
|
120204
|
+
}
|
|
120205
|
+
throw error88;
|
|
120206
|
+
}
|
|
120207
|
+
const update2 = buildAssessmentAttemptSupersessionUpdate(result);
|
|
120208
|
+
if (!update2) {
|
|
120209
|
+
return;
|
|
120210
|
+
}
|
|
120211
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(attemptId, update2);
|
|
120212
|
+
addEvent("assessment.attempt_superseded", {
|
|
120213
|
+
"app.assessment.attempt_id": attemptId,
|
|
120214
|
+
"app.assessment.reason": "selected_test_version_unavailable"
|
|
120215
|
+
});
|
|
120216
|
+
});
|
|
120217
|
+
}
|
|
120218
|
+
}
|
|
120219
|
+
async persistPendingSupersessionsBestEffort(db2, attemptIds) {
|
|
120220
|
+
if (attemptIds.size === 0) {
|
|
120221
|
+
return;
|
|
120222
|
+
}
|
|
120223
|
+
try {
|
|
120224
|
+
await this.persistPendingSupersessions(db2, attemptIds);
|
|
120225
|
+
} catch (error88) {
|
|
120226
|
+
addEvent("assessment.attempt_supersession_persist_failed", {
|
|
120227
|
+
"app.assessment.attempt_ids": [...attemptIds].toSorted().join(","),
|
|
120228
|
+
"exception.type": errorType(error88),
|
|
120229
|
+
"app.error.message": errorMessage2(error88)
|
|
120230
|
+
});
|
|
120231
|
+
}
|
|
120232
|
+
}
|
|
119976
120233
|
async resumeReviewSnapshot(decision, attempts, existingChildResults, context2) {
|
|
119977
120234
|
if (decision.additionalResumableAttemptIds.length > 0) {
|
|
119978
120235
|
addEvent("assessment.multiple_resumable_attempts", {
|
|
@@ -119984,7 +120241,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
119984
120241
|
if (resumed.metadata.purpose !== "review") {
|
|
119985
120242
|
throw new Error("A standards-review request selected a non-review attempt");
|
|
119986
120243
|
}
|
|
119987
|
-
const
|
|
120244
|
+
const catalog = await this.loadReviewBankCatalog(resumed.metadata.selectedTest.identifier, resumed.metadata.selectedTest.contentRevision);
|
|
120245
|
+
const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
|
|
119988
120246
|
const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
|
|
119989
120247
|
const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
|
|
119990
120248
|
await this.ensureReviewChildResults({
|
|
@@ -119998,20 +120256,68 @@ class TimebackAssessmentRuntimeService {
|
|
|
119998
120256
|
});
|
|
119999
120257
|
return this.snapshot(resumed.result, resumed.metadata, assessment, context2.integration);
|
|
120000
120258
|
}
|
|
120001
|
-
|
|
120259
|
+
recordReviewPreparationPhase(phase, startedAt, counts = {}) {
|
|
120260
|
+
addEvent("assessment.review_preparation_phase", {
|
|
120261
|
+
"app.assessment.review_phase": phase,
|
|
120262
|
+
"app.assessment.duration_ms": Date.now() - startedAt,
|
|
120263
|
+
...counts.candidateCount === undefined ? {} : { "app.assessment.candidate_count": counts.candidateCount },
|
|
120264
|
+
...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
|
|
120265
|
+
});
|
|
120266
|
+
}
|
|
120267
|
+
async loadReviewBankCatalog(identifier, expectedSourceRevision) {
|
|
120002
120268
|
if (expectedSourceRevision) {
|
|
120003
120269
|
const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
|
|
120004
120270
|
if (cached3) {
|
|
120005
120271
|
return cached3;
|
|
120006
120272
|
}
|
|
120007
120273
|
}
|
|
120008
|
-
|
|
120009
|
-
|
|
120010
|
-
|
|
120011
|
-
|
|
120012
|
-
|
|
120013
|
-
|
|
120014
|
-
|
|
120274
|
+
let loaded;
|
|
120275
|
+
try {
|
|
120276
|
+
loaded = await loadQtiTestReferences(this.requireClient(), identifier);
|
|
120277
|
+
} catch (error88) {
|
|
120278
|
+
if (expectedSourceRevision && isApiError(error88) && error88.statusCode === 404) {
|
|
120279
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, { identifier, expectedRevision: expectedSourceRevision });
|
|
120280
|
+
}
|
|
120281
|
+
throw error88;
|
|
120282
|
+
}
|
|
120283
|
+
let bank;
|
|
120284
|
+
try {
|
|
120285
|
+
bank = await reviewBankIndexFromManifest(loaded.test.metadata, {
|
|
120286
|
+
bankIdentifier: identifier,
|
|
120287
|
+
membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
|
|
120288
|
+
});
|
|
120289
|
+
} catch (error88) {
|
|
120290
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Update Mapping in assessment authoring.`, { reason: errorMessage2(error88) });
|
|
120291
|
+
}
|
|
120292
|
+
if (!bank) {
|
|
120293
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
|
|
120294
|
+
}
|
|
120295
|
+
if (expectedSourceRevision && bank.sourceContentRevision !== expectedSourceRevision) {
|
|
120296
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, {
|
|
120297
|
+
identifier,
|
|
120298
|
+
expectedRevision: expectedSourceRevision,
|
|
120299
|
+
currentRevision: bank.sourceContentRevision
|
|
120300
|
+
});
|
|
120301
|
+
}
|
|
120302
|
+
const catalog = { ...loaded, bank };
|
|
120303
|
+
this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
|
|
120304
|
+
return catalog;
|
|
120305
|
+
}
|
|
120306
|
+
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
120307
|
+
const cacheKey2 = [
|
|
120308
|
+
catalog.test.identifier,
|
|
120309
|
+
catalog.bank.sourceContentRevision,
|
|
120310
|
+
...itemIdentifiers
|
|
120311
|
+
].join("\x00");
|
|
120312
|
+
const cached3 = this.reviewSelectionCache.get(cacheKey2);
|
|
120313
|
+
if (cached3) {
|
|
120314
|
+
return { assessment: cached3, bank: catalog.bank };
|
|
120315
|
+
}
|
|
120316
|
+
const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
|
|
120317
|
+
const assessment = await buildPlayableAssessment(catalog.test, questions);
|
|
120318
|
+
assessment.contentRevision = catalog.bank.sourceContentRevision;
|
|
120319
|
+
this.reviewSelectionCache.set(cacheKey2, assessment);
|
|
120320
|
+
return { assessment, bank: catalog.bank };
|
|
120015
120321
|
}
|
|
120016
120322
|
pinnedReviewBankSource(source, metadata2) {
|
|
120017
120323
|
return {
|
|
@@ -120042,7 +120348,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
120042
120348
|
if (metadata2.purpose !== "review") {
|
|
120043
120349
|
return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
120044
120350
|
}
|
|
120045
|
-
const
|
|
120351
|
+
const catalog = await this.loadReviewBankCatalog(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
120352
|
+
const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
|
|
120046
120353
|
const source = this.pinnedReviewBankSource(currentSource, metadata2);
|
|
120047
120354
|
return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
|
|
120048
120355
|
}
|
|
@@ -120540,7 +120847,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
120540
120847
|
async crossAttemptLockBarrier(attemptId) {
|
|
120541
120848
|
for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
|
|
120542
120849
|
try {
|
|
120543
|
-
await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(attemptId), async () => {
|
|
120850
|
+
await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
120544
120851
|
return;
|
|
120545
120852
|
});
|
|
120546
120853
|
return;
|
|
@@ -120665,6 +120972,14 @@ class TimebackAssessmentRuntimeService {
|
|
|
120665
120972
|
return AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
|
|
120666
120973
|
}
|
|
120667
120974
|
async snapshotForResult(result, metadata2, integration) {
|
|
120975
|
+
if (isAssessmentAttemptSuperseded({
|
|
120976
|
+
inProgress: result.inProgress ?? "",
|
|
120977
|
+
scoreStatus: result.scoreStatus
|
|
120978
|
+
})) {
|
|
120979
|
+
throw new GoneError("This assessment attempt has been superseded.", {
|
|
120980
|
+
attemptId: result.sourcedId
|
|
120981
|
+
});
|
|
120982
|
+
}
|
|
120668
120983
|
const assessment = await this.loadAttemptAssessment(metadata2);
|
|
120669
120984
|
if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
|
|
120670
120985
|
const routing = await this.loadAttemptDiagnosticRouting(metadata2, assessment);
|
|
@@ -122010,6 +122325,60 @@ function buildQtiLibraryListPlan(params) {
|
|
|
122010
122325
|
}
|
|
122011
122326
|
var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-";
|
|
122012
122327
|
var PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
|
|
122328
|
+
function reviewMappingIssueSummary(label, identifiers) {
|
|
122329
|
+
if (identifiers.length === 0) {
|
|
122330
|
+
return null;
|
|
122331
|
+
}
|
|
122332
|
+
const displayed = identifiers.slice(0, 10).join(", ");
|
|
122333
|
+
const remainder = identifiers.length > 10 ? `, and ${identifiers.length - 10} more` : "";
|
|
122334
|
+
return `${label} (${identifiers.length}): ${displayed}${remainder}`;
|
|
122335
|
+
}
|
|
122336
|
+
async function prepareReviewMappingUpdate(test, questions) {
|
|
122337
|
+
const contentRevision = await assessmentContentRevision(test, questions);
|
|
122338
|
+
const assessment = {
|
|
122339
|
+
identifier: test.identifier,
|
|
122340
|
+
contractVersion: 1,
|
|
122341
|
+
contentRevision,
|
|
122342
|
+
title: test.title,
|
|
122343
|
+
items: questions.questions.map(({ question }) => ({
|
|
122344
|
+
identifier: question.identifier,
|
|
122345
|
+
title: question.title,
|
|
122346
|
+
prompt: "",
|
|
122347
|
+
maxScore: 1,
|
|
122348
|
+
interactions: []
|
|
122349
|
+
}))
|
|
122350
|
+
};
|
|
122351
|
+
const bank = await buildReviewBankIndex(assessment, questions, { customOnly: true });
|
|
122352
|
+
const issues = reviewBankMappingIssues(bank);
|
|
122353
|
+
if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
|
|
122354
|
+
const details = [
|
|
122355
|
+
reviewMappingIssueSummary("missing or invalid associations", issues.missingOrInvalidItemIdentifiers),
|
|
122356
|
+
reviewMappingIssueSummary("multiple associations", issues.multipleStandardItemIdentifiers)
|
|
122357
|
+
].filter((detail) => detail !== null);
|
|
122358
|
+
throw new ValidationError(`Every review question must have exactly one valid standard/node association. ${details.join("; ")}.`);
|
|
122359
|
+
}
|
|
122360
|
+
const manifest = await buildReviewBankManifest(bank);
|
|
122361
|
+
return {
|
|
122362
|
+
update: {
|
|
122363
|
+
...buildQtiTestUpdateInput(test, test.title),
|
|
122364
|
+
metadata: {
|
|
122365
|
+
...test.metadata,
|
|
122366
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: manifest
|
|
122367
|
+
}
|
|
122368
|
+
},
|
|
122369
|
+
summary: {
|
|
122370
|
+
itemCount: bank.items.length,
|
|
122371
|
+
standardCount: Object.keys(manifest.itemsByStandard).length,
|
|
122372
|
+
sourceFingerprint: manifest.sourceFingerprint
|
|
122373
|
+
}
|
|
122374
|
+
};
|
|
122375
|
+
}
|
|
122376
|
+
var init_timeback_review_mapping_util = __esm(() => {
|
|
122377
|
+
init_assessment_runtime2();
|
|
122378
|
+
init_errors2();
|
|
122379
|
+
init_timeback_assessment_runtime_util();
|
|
122380
|
+
init_timeback_qti_authoring_util();
|
|
122381
|
+
});
|
|
122013
122382
|
function databaseConstraintName(error88) {
|
|
122014
122383
|
if (typeof error88 !== "object" || error88 === null) {
|
|
122015
122384
|
return;
|
|
@@ -122091,7 +122460,8 @@ class TimebackAssessmentsService {
|
|
|
122091
122460
|
ownerGameSlug: ownership.gameSlug,
|
|
122092
122461
|
integrationId,
|
|
122093
122462
|
subject: integration.subject,
|
|
122094
|
-
grade: String(integration.grade)
|
|
122463
|
+
grade: String(integration.grade),
|
|
122464
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.qtiTestIdentifier)
|
|
122095
122465
|
}
|
|
122096
122466
|
});
|
|
122097
122467
|
try {
|
|
@@ -122160,7 +122530,9 @@ class TimebackAssessmentsService {
|
|
|
122160
122530
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
122161
122531
|
assertPlayableAssessmentImportQuestions(manifest.targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
122162
122532
|
if (entry2.purpose === "review") {
|
|
122163
|
-
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) =>
|
|
122533
|
+
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
|
|
122534
|
+
customOnly: true
|
|
122535
|
+
}).length));
|
|
122164
122536
|
}
|
|
122165
122537
|
return { entry: entry2, test: loaded.test };
|
|
122166
122538
|
} catch (error88) {
|
|
@@ -122312,6 +122684,10 @@ class TimebackAssessmentsService {
|
|
|
122312
122684
|
if (input.status !== undefined) {
|
|
122313
122685
|
validateAssessmentStatusTransition(row.status, input.status);
|
|
122314
122686
|
}
|
|
122687
|
+
if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
|
|
122688
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
122689
|
+
await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, ownership.gameSlug);
|
|
122690
|
+
}
|
|
122315
122691
|
if (input.title !== undefined) {
|
|
122316
122692
|
assertDraftAssessment(row);
|
|
122317
122693
|
assertAllAssessmentAssociationsDraft(associations);
|
|
@@ -122330,7 +122706,8 @@ class TimebackAssessmentsService {
|
|
|
122330
122706
|
updates.diagnosticKey = nextDiagnostic.diagnosticKey;
|
|
122331
122707
|
updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
|
|
122332
122708
|
} else {
|
|
122333
|
-
|
|
122709
|
+
const reviewOwnership = nextPurpose === "review" && nextStatus === "live" ? await this.requireQtiTestOwnershipContext(integrationId, tx) : undefined;
|
|
122710
|
+
await this.validateAssessmentHasQuestions(qtiTestIdentifier, reviewOwnership?.gameSlug);
|
|
122334
122711
|
}
|
|
122335
122712
|
}
|
|
122336
122713
|
let updated = row;
|
|
@@ -122425,6 +122802,19 @@ class TimebackAssessmentsService {
|
|
|
122425
122802
|
});
|
|
122426
122803
|
return { ...result, questions };
|
|
122427
122804
|
}
|
|
122805
|
+
async updateReviewMapping(integrationId, qtiTestIdentifier) {
|
|
122806
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
122807
|
+
if (!associations.some((association) => association.purpose === "review")) {
|
|
122808
|
+
throw new ValidationError("Review mapping is available only for standards-review assessments.");
|
|
122809
|
+
}
|
|
122810
|
+
const client2 = this.requireClient();
|
|
122811
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
122812
|
+
const result = await this.rebuildReviewMapping(client2, qtiTestIdentifier, ownership.gameSlug);
|
|
122813
|
+
setAttribute("app.assessment.operation", "update_review_mapping");
|
|
122814
|
+
setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
|
|
122815
|
+
return result;
|
|
122816
|
+
});
|
|
122817
|
+
}
|
|
122428
122818
|
async listQuestionLibrary(integrationId, params) {
|
|
122429
122819
|
const client2 = this.requireClient();
|
|
122430
122820
|
await this.requireIntegration(integrationId);
|
|
@@ -122465,7 +122855,8 @@ class TimebackAssessmentsService {
|
|
|
122465
122855
|
integrationId,
|
|
122466
122856
|
subject: integration.subject,
|
|
122467
122857
|
grade: String(integration.grade),
|
|
122468
|
-
copiedFromTestIdentifier: sourceTestIdentifier
|
|
122858
|
+
copiedFromTestIdentifier: sourceTestIdentifier,
|
|
122859
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(targetTestIdentifier)
|
|
122469
122860
|
}, itemPlan);
|
|
122470
122861
|
const attemptedItemIdentifiers = [];
|
|
122471
122862
|
let testCreationAttempted = false;
|
|
@@ -122853,16 +123244,20 @@ class TimebackAssessmentsService {
|
|
|
122853
123244
|
const plan = buildQtiLibraryListPlan(params);
|
|
122854
123245
|
return list(plan.params);
|
|
122855
123246
|
}
|
|
122856
|
-
async validateAssessmentHasQuestions(qtiTestIdentifier,
|
|
123247
|
+
async validateAssessmentHasQuestions(qtiTestIdentifier, reviewGameSlug) {
|
|
122857
123248
|
const client2 = this.requireClient();
|
|
122858
|
-
const result = await
|
|
123249
|
+
const [test, result] = await Promise.all([
|
|
123250
|
+
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
123251
|
+
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
123252
|
+
]);
|
|
122859
123253
|
assertAssessmentHasQuestions(result.questions);
|
|
122860
123254
|
const hydrated = await hydrateQtiTestQuestions(client2, result);
|
|
122861
123255
|
for (const { question } of hydrated.questions) {
|
|
122862
123256
|
assertPlayableQtiQuestion(question);
|
|
122863
123257
|
}
|
|
122864
|
-
if (
|
|
122865
|
-
|
|
123258
|
+
if (reviewGameSlug) {
|
|
123259
|
+
assertQtiTestOwnedByGame(test, reviewGameSlug);
|
|
123260
|
+
await this.writeReviewMapping(client2, test, hydrated);
|
|
122866
123261
|
}
|
|
122867
123262
|
}
|
|
122868
123263
|
async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
|
|
@@ -122927,6 +123322,19 @@ class TimebackAssessmentsService {
|
|
|
122927
123322
|
routingManifest: validation.manifest
|
|
122928
123323
|
};
|
|
122929
123324
|
}
|
|
123325
|
+
async rebuildReviewMapping(client2, qtiTestIdentifier, gameSlug) {
|
|
123326
|
+
const [test, references] = await Promise.all([
|
|
123327
|
+
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
123328
|
+
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
123329
|
+
]);
|
|
123330
|
+
assertQtiTestOwnedByGame(test, gameSlug);
|
|
123331
|
+
return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
|
|
123332
|
+
}
|
|
123333
|
+
async writeReviewMapping(client2, test, questions) {
|
|
123334
|
+
const prepared = await prepareReviewMappingUpdate(test, questions);
|
|
123335
|
+
await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
|
|
123336
|
+
return prepared.summary;
|
|
123337
|
+
}
|
|
122930
123338
|
}
|
|
122931
123339
|
var init_timeback_assessments_service = __esm(async () => {
|
|
122932
123340
|
init_drizzle_orm();
|
|
@@ -122934,7 +123342,6 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
122934
123342
|
init_tables_index();
|
|
122935
123343
|
init_spans();
|
|
122936
123344
|
init_assessment_runtime2();
|
|
122937
|
-
init_qti();
|
|
122938
123345
|
init_timeback3();
|
|
122939
123346
|
init_errors2();
|
|
122940
123347
|
init_timeback_assessment_import_util();
|
|
@@ -122942,6 +123349,7 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
122942
123349
|
init_timeback_assessment_runtime_util();
|
|
122943
123350
|
init_timeback_qti_authoring_util();
|
|
122944
123351
|
init_timeback_qti_hydration_util();
|
|
123352
|
+
init_timeback_review_mapping_util();
|
|
122945
123353
|
await init_errors8();
|
|
122946
123354
|
});
|
|
122947
123355
|
function buildTimebackBaseConfigFromExistingConfig(config5) {
|
|
@@ -124928,7 +125336,7 @@ var init_upload_service = __esm(() => {
|
|
|
124928
125336
|
function createPlatformServices(deps) {
|
|
124929
125337
|
const {
|
|
124930
125338
|
db: db2,
|
|
124931
|
-
|
|
125339
|
+
assessmentRuntimeLock,
|
|
124932
125340
|
config: config5,
|
|
124933
125341
|
cloudflare: cloudflare2,
|
|
124934
125342
|
corsKvs,
|
|
@@ -124997,7 +125405,7 @@ function createPlatformServices(deps) {
|
|
|
124997
125405
|
db: db2,
|
|
124998
125406
|
timeback: timebackClient,
|
|
124999
125407
|
validateDeveloperAccess,
|
|
125000
|
-
assessmentRuntimeLock
|
|
125408
|
+
assessmentRuntimeLock
|
|
125001
125409
|
});
|
|
125002
125410
|
return {
|
|
125003
125411
|
bucket,
|
|
@@ -125015,7 +125423,6 @@ function createPlatformServices(deps) {
|
|
|
125015
125423
|
};
|
|
125016
125424
|
}
|
|
125017
125425
|
var init_platform2 = __esm(async () => {
|
|
125018
|
-
init_assessment_runtime_lock_util();
|
|
125019
125426
|
init_bucket_service();
|
|
125020
125427
|
init_database_service();
|
|
125021
125428
|
init_deployment_state_service();
|
|
@@ -125695,7 +126102,7 @@ var init_standalone = __esm(() => {
|
|
|
125695
126102
|
init_verify_service();
|
|
125696
126103
|
});
|
|
125697
126104
|
function createServices(ctx) {
|
|
125698
|
-
const { db: db2,
|
|
126105
|
+
const { db: db2, assessmentRuntimeLock, config: config5, providers, cloudflare: cloudflare2, timeback: timeback2, discord, corsKvs } = ctx;
|
|
125699
126106
|
const { auth: auth2, storage, r2Storage, cache } = providers;
|
|
125700
126107
|
const infra2 = createInfraServices({
|
|
125701
126108
|
db: db2,
|
|
@@ -125717,7 +126124,7 @@ function createServices(ctx) {
|
|
|
125717
126124
|
});
|
|
125718
126125
|
const platform2 = createPlatformServices({
|
|
125719
126126
|
db: db2,
|
|
125720
|
-
|
|
126127
|
+
assessmentRuntimeLock,
|
|
125721
126128
|
config: config5,
|
|
125722
126129
|
cloudflare: cloudflare2,
|
|
125723
126130
|
corsKvs,
|
|
@@ -125744,6 +126151,324 @@ var init_factory = __esm(async () => {
|
|
|
125744
126151
|
init_standalone();
|
|
125745
126152
|
await init_platform2();
|
|
125746
126153
|
});
|
|
126154
|
+
function isAuthenticated(ctx) {
|
|
126155
|
+
return ctx.user != null;
|
|
126156
|
+
}
|
|
126157
|
+
var init_types5 = () => {};
|
|
126158
|
+
function hasGameManagementAccess(user) {
|
|
126159
|
+
return user.role === "admin" || user.role === "teacher" || user.role === "developer" && user.developerStatus === "approved";
|
|
126160
|
+
}
|
|
126161
|
+
function isDashboardWorkerKey(key) {
|
|
126162
|
+
return Boolean(key?.permissions?.[DASHBOARD_PERMISSION_NAMESPACE]);
|
|
126163
|
+
}
|
|
126164
|
+
function workerKeyGrantsGameRead(key, slug2) {
|
|
126165
|
+
if (!isDashboardWorkerKey(key) && !isGameWorkerKey(key)) {
|
|
126166
|
+
return false;
|
|
126167
|
+
}
|
|
126168
|
+
const games2 = key?.permissions?.games;
|
|
126169
|
+
return Boolean(games2?.includes(`read:${slug2}`) || games2?.includes(`write:${slug2}`));
|
|
126170
|
+
}
|
|
126171
|
+
function isGameWorkerKey(key) {
|
|
126172
|
+
return Boolean(key?.name?.startsWith(GAME_WORKER_KEY_PREFIX));
|
|
126173
|
+
}
|
|
126174
|
+
function deniedWorkerKeyRequest(key, method, pathname) {
|
|
126175
|
+
if (isDashboardWorkerKey(key) && !isAllowedDashboardWorkerRequest(method, pathname, key.permissions)) {
|
|
126176
|
+
return DASHBOARD_WORKER_KEY_RESTRICTED;
|
|
126177
|
+
}
|
|
126178
|
+
return null;
|
|
126179
|
+
}
|
|
126180
|
+
function rejectDashboardWorkerKey(ctx) {
|
|
126181
|
+
if (isDashboardWorkerKey(ctx.apiKey)) {
|
|
126182
|
+
throw ApiError.forbidden(DASHBOARD_WORKER_KEY_RESTRICTED);
|
|
126183
|
+
}
|
|
126184
|
+
}
|
|
126185
|
+
function assertAuthenticatedRequest(ctx) {
|
|
126186
|
+
if (!isAuthenticated(ctx)) {
|
|
126187
|
+
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
126188
|
+
}
|
|
126189
|
+
if (ctx.apiKey) {
|
|
126190
|
+
const denied = deniedWorkerKeyRequest(ctx.apiKey, ctx.request.method, ctx.url.pathname);
|
|
126191
|
+
if (denied) {
|
|
126192
|
+
throw ApiError.forbidden(denied);
|
|
126193
|
+
}
|
|
126194
|
+
}
|
|
126195
|
+
}
|
|
126196
|
+
function requireAuth(handler) {
|
|
126197
|
+
return async (ctx) => {
|
|
126198
|
+
assertAuthenticatedRequest(ctx);
|
|
126199
|
+
return handler(ctx);
|
|
126200
|
+
};
|
|
126201
|
+
}
|
|
126202
|
+
function requireNonAnonymous(handler) {
|
|
126203
|
+
return async (ctx) => {
|
|
126204
|
+
assertAuthenticatedRequest(ctx);
|
|
126205
|
+
if (ctx.user.isAnonymous) {
|
|
126206
|
+
throw ApiError.forbidden("This operation is not available for demo/anonymous users");
|
|
126207
|
+
}
|
|
126208
|
+
return handler(ctx);
|
|
126209
|
+
};
|
|
126210
|
+
}
|
|
126211
|
+
function requireAnonymous(handler) {
|
|
126212
|
+
return async (ctx) => {
|
|
126213
|
+
assertAuthenticatedRequest(ctx);
|
|
126214
|
+
if (!ctx.user.isAnonymous) {
|
|
126215
|
+
throw ApiError.forbidden("This operation is only available for demo/anonymous users");
|
|
126216
|
+
}
|
|
126217
|
+
return handler(ctx);
|
|
126218
|
+
};
|
|
126219
|
+
}
|
|
126220
|
+
function requireDeveloper(handler) {
|
|
126221
|
+
return async (ctx) => {
|
|
126222
|
+
assertAuthenticatedRequest(ctx);
|
|
126223
|
+
rejectDashboardWorkerKey(ctx);
|
|
126224
|
+
const isAdmin = ctx.user.role === "admin";
|
|
126225
|
+
const isApprovedDev = ctx.user.role === "developer" && ctx.user.developerStatus === "approved";
|
|
126226
|
+
if (!isAdmin && !isApprovedDev) {
|
|
126227
|
+
throw ApiError.forbidden("Must be an approved developer");
|
|
126228
|
+
}
|
|
126229
|
+
return handler(ctx);
|
|
126230
|
+
};
|
|
126231
|
+
}
|
|
126232
|
+
function requireHumanDeveloper(handler) {
|
|
126233
|
+
return requireDeveloper(async (ctx) => {
|
|
126234
|
+
if (isDashboardWorkerKey(ctx.apiKey) || isGameWorkerKey(ctx.apiKey)) {
|
|
126235
|
+
throw ApiError.forbidden(WORKER_KEY_HUMAN_ONLY);
|
|
126236
|
+
}
|
|
126237
|
+
return handler(ctx);
|
|
126238
|
+
});
|
|
126239
|
+
}
|
|
126240
|
+
function requireGameManagementAccess(handler) {
|
|
126241
|
+
return async (ctx) => {
|
|
126242
|
+
assertAuthenticatedRequest(ctx);
|
|
126243
|
+
rejectDashboardWorkerKey(ctx);
|
|
126244
|
+
if (!hasGameManagementAccess(ctx.user)) {
|
|
126245
|
+
throw ApiError.forbidden("Game management access required");
|
|
126246
|
+
}
|
|
126247
|
+
return handler(ctx);
|
|
126248
|
+
};
|
|
126249
|
+
}
|
|
126250
|
+
var WORKER_KEY_HUMAN_ONLY = "This operation requires your own credential — platform worker keys are refused";
|
|
126251
|
+
var init_auth_util = __esm(() => {
|
|
126252
|
+
init_errors2();
|
|
126253
|
+
init_types5();
|
|
126254
|
+
init_dashboard_util();
|
|
126255
|
+
init_deployment_util();
|
|
126256
|
+
});
|
|
126257
|
+
function defineControllerNames(namespace, handlers) {
|
|
126258
|
+
for (const [key, handler] of Object.entries(handlers)) {
|
|
126259
|
+
Object.defineProperty(handler, "name", {
|
|
126260
|
+
value: `${namespace}.${key}`,
|
|
126261
|
+
configurable: true
|
|
126262
|
+
});
|
|
126263
|
+
}
|
|
126264
|
+
return handlers;
|
|
126265
|
+
}
|
|
126266
|
+
function generateUsername(email5) {
|
|
126267
|
+
const baseUsername = (email5.split("@")[0] || "user").toLowerCase();
|
|
126268
|
+
const cleanUsername = baseUsername.replace(/[^a-z0-9]/g, "");
|
|
126269
|
+
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
126270
|
+
return `${cleanUsername}_${randomSuffix}`;
|
|
126271
|
+
}
|
|
126272
|
+
function extractRedirectPath(targetUri, currentHost) {
|
|
126273
|
+
try {
|
|
126274
|
+
const targetUrl = new URL(targetUri);
|
|
126275
|
+
if (targetUrl.hostname === currentHost) {
|
|
126276
|
+
return targetUrl.pathname + targetUrl.search;
|
|
126277
|
+
}
|
|
126278
|
+
} catch {}
|
|
126279
|
+
return "/";
|
|
126280
|
+
}
|
|
126281
|
+
function validateLtiClaims(claims) {
|
|
126282
|
+
const messageType = claims["https://purl.imsglobal.org/spec/lti/claim/message_type"];
|
|
126283
|
+
const version4 = claims["https://purl.imsglobal.org/spec/lti/claim/version"];
|
|
126284
|
+
if (messageType !== "LtiResourceLinkRequest") {
|
|
126285
|
+
return `Invalid LTI message type: ${messageType}`;
|
|
126286
|
+
}
|
|
126287
|
+
if (version4 !== "1.3.0") {
|
|
126288
|
+
return `Unsupported LTI version: ${version4}`;
|
|
126289
|
+
}
|
|
126290
|
+
return null;
|
|
126291
|
+
}
|
|
126292
|
+
var init_lti_util = () => {};
|
|
126293
|
+
async function provisionLtiUser(db2, claims) {
|
|
126294
|
+
const database = db2;
|
|
126295
|
+
const email5 = claims.email;
|
|
126296
|
+
const ltiTimebackId = claims.sub;
|
|
126297
|
+
const providerId = AUTH_PROVIDER_IDS.TIMEBACK_LTI;
|
|
126298
|
+
if (!email5) {
|
|
126299
|
+
throw new ValidationError("Email is required in LTI claims");
|
|
126300
|
+
}
|
|
126301
|
+
const existingAccount = await database.query.accounts.findFirst({
|
|
126302
|
+
where: and(eq(accounts.accountId, ltiTimebackId), eq(accounts.providerId, providerId))
|
|
126303
|
+
});
|
|
126304
|
+
if (existingAccount) {
|
|
126305
|
+
const user = await database.query.users.findFirst({
|
|
126306
|
+
where: eq(users.id, existingAccount.userId)
|
|
126307
|
+
});
|
|
126308
|
+
if (user) {
|
|
126309
|
+
setAttribute("app.lti.provision", "existing_account");
|
|
126310
|
+
return user;
|
|
126311
|
+
}
|
|
126312
|
+
}
|
|
126313
|
+
const existingUser = await database.query.users.findFirst({
|
|
126314
|
+
where: eq(users.email, email5)
|
|
126315
|
+
});
|
|
126316
|
+
if (existingUser) {
|
|
126317
|
+
await database.transaction(async (tx) => {
|
|
126318
|
+
const existingLtiAccount = await tx.query.accounts.findFirst({
|
|
126319
|
+
where: and(eq(accounts.userId, existingUser.id), eq(accounts.providerId, providerId))
|
|
126320
|
+
});
|
|
126321
|
+
if (!existingLtiAccount) {
|
|
126322
|
+
const [account] = await tx.insert(accounts).values({
|
|
126323
|
+
id: crypto4.randomUUID(),
|
|
126324
|
+
userId: existingUser.id,
|
|
126325
|
+
accountId: ltiTimebackId,
|
|
126326
|
+
providerId,
|
|
126327
|
+
accessToken: null,
|
|
126328
|
+
refreshToken: null,
|
|
126329
|
+
accessTokenExpiresAt: null,
|
|
126330
|
+
refreshTokenExpiresAt: null,
|
|
126331
|
+
createdAt: new Date,
|
|
126332
|
+
updatedAt: new Date
|
|
126333
|
+
}).returning({ id: accounts.id });
|
|
126334
|
+
if (!account) {
|
|
126335
|
+
throw new InternalError("Failed to link LTI account");
|
|
126336
|
+
}
|
|
126337
|
+
}
|
|
126338
|
+
});
|
|
126339
|
+
setAttribute("app.lti.provision", "linked_account");
|
|
126340
|
+
return existingUser;
|
|
126341
|
+
}
|
|
126342
|
+
const newUserId = crypto4.randomUUID();
|
|
126343
|
+
const createdUser = await database.transaction(async (tx) => {
|
|
126344
|
+
const [insertedUser] = await tx.insert(users).values({
|
|
126345
|
+
id: newUserId,
|
|
126346
|
+
email: email5,
|
|
126347
|
+
emailVerified: true,
|
|
126348
|
+
username: generateUsername(email5),
|
|
126349
|
+
name: claims.name || claims.given_name || email5.split("@")[0] || "Timeback User",
|
|
126350
|
+
createdAt: new Date,
|
|
126351
|
+
updatedAt: new Date
|
|
126352
|
+
}).returning();
|
|
126353
|
+
if (!insertedUser) {
|
|
126354
|
+
throw new InternalError("Failed to create user");
|
|
126355
|
+
}
|
|
126356
|
+
await tx.insert(accounts).values({
|
|
126357
|
+
id: crypto4.randomUUID(),
|
|
126358
|
+
userId: newUserId,
|
|
126359
|
+
accountId: ltiTimebackId,
|
|
126360
|
+
providerId,
|
|
126361
|
+
accessToken: null,
|
|
126362
|
+
refreshToken: null,
|
|
126363
|
+
accessTokenExpiresAt: null,
|
|
126364
|
+
refreshTokenExpiresAt: null,
|
|
126365
|
+
createdAt: new Date,
|
|
126366
|
+
updatedAt: new Date
|
|
126367
|
+
});
|
|
126368
|
+
setAttribute("app.lti.provision", "created_user");
|
|
126369
|
+
return insertedUser;
|
|
126370
|
+
});
|
|
126371
|
+
return createdUser;
|
|
126372
|
+
}
|
|
126373
|
+
var init_lti_provisioning = __esm(() => {
|
|
126374
|
+
init_drizzle_orm();
|
|
126375
|
+
init_src();
|
|
126376
|
+
init_tables_index();
|
|
126377
|
+
init_spans();
|
|
126378
|
+
init_errors2();
|
|
126379
|
+
init_lti_util();
|
|
126380
|
+
});
|
|
126381
|
+
function requireGameId(gameId) {
|
|
126382
|
+
if (!gameId) {
|
|
126383
|
+
throw ApiError.badRequest("Missing game ID");
|
|
126384
|
+
}
|
|
126385
|
+
if (!isValidUUID(gameId)) {
|
|
126386
|
+
throw ApiError.unprocessableEntity("gameId must be a valid UUID format");
|
|
126387
|
+
}
|
|
126388
|
+
return gameId;
|
|
126389
|
+
}
|
|
126390
|
+
function requireSlug(slug2) {
|
|
126391
|
+
if (!slug2) {
|
|
126392
|
+
throw ApiError.badRequest("Missing game slug");
|
|
126393
|
+
}
|
|
126394
|
+
return slug2;
|
|
126395
|
+
}
|
|
126396
|
+
function requireUserId(userId) {
|
|
126397
|
+
if (!userId) {
|
|
126398
|
+
throw ApiError.badRequest("Missing user ID");
|
|
126399
|
+
}
|
|
126400
|
+
if (!isValidUUID(userId)) {
|
|
126401
|
+
throw ApiError.unprocessableEntity("userId must be a valid UUID format");
|
|
126402
|
+
}
|
|
126403
|
+
return userId;
|
|
126404
|
+
}
|
|
126405
|
+
function parseLimitParam(url4, max) {
|
|
126406
|
+
const raw = url4.searchParams.get("limit");
|
|
126407
|
+
if (raw === null) {
|
|
126408
|
+
return;
|
|
126409
|
+
}
|
|
126410
|
+
const limit = Number(raw);
|
|
126411
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > max) {
|
|
126412
|
+
throw ApiError.badRequest(`limit must be an integer between 1 and ${max}`);
|
|
126413
|
+
}
|
|
126414
|
+
return limit;
|
|
126415
|
+
}
|
|
126416
|
+
var init_params_util = __esm(() => {
|
|
126417
|
+
init_src9();
|
|
126418
|
+
init_errors2();
|
|
126419
|
+
});
|
|
126420
|
+
function formatZodError(error88) {
|
|
126421
|
+
const flat = error88.flatten();
|
|
126422
|
+
const result = {};
|
|
126423
|
+
if (Object.keys(flat.fieldErrors).length > 0) {
|
|
126424
|
+
result.fields = {};
|
|
126425
|
+
for (const [field, messages] of Object.entries(flat.fieldErrors)) {
|
|
126426
|
+
if (messages && messages.length > 0) {
|
|
126427
|
+
result.fields[field] = messages[0];
|
|
126428
|
+
}
|
|
126429
|
+
}
|
|
126430
|
+
}
|
|
126431
|
+
if (flat.formErrors.length > 0) {
|
|
126432
|
+
result.errors = flat.formErrors;
|
|
126433
|
+
}
|
|
126434
|
+
return result;
|
|
126435
|
+
}
|
|
126436
|
+
function recordValidationFailure(details) {
|
|
126437
|
+
setAttribute("app.request.outcome", "validation_failed");
|
|
126438
|
+
addEvent("request.validation_failed", {
|
|
126439
|
+
"app.validation.error": JSON.stringify(details)
|
|
126440
|
+
});
|
|
126441
|
+
}
|
|
126442
|
+
async function parseRequestBody(request, schema2) {
|
|
126443
|
+
try {
|
|
126444
|
+
return schema2.parse(await request.json());
|
|
126445
|
+
} catch (error88) {
|
|
126446
|
+
if (error88 instanceof exports_external.ZodError) {
|
|
126447
|
+
const details = formatZodError(error88);
|
|
126448
|
+
recordValidationFailure(details);
|
|
126449
|
+
throw ApiError.unprocessableEntity("Validation failed", details);
|
|
126450
|
+
}
|
|
126451
|
+
throw ApiError.invalidJsonBody();
|
|
126452
|
+
}
|
|
126453
|
+
}
|
|
126454
|
+
var init_validation_util = __esm(() => {
|
|
126455
|
+
init_esm();
|
|
126456
|
+
init_spans();
|
|
126457
|
+
init_errors2();
|
|
126458
|
+
});
|
|
126459
|
+
var init_utils7 = __esm(() => {
|
|
126460
|
+
init_auth_util();
|
|
126461
|
+
init_assessment_runtime_lock_util();
|
|
126462
|
+
init_dashboard_util();
|
|
126463
|
+
init_deployment_util();
|
|
126464
|
+
init_leaderboard_util();
|
|
126465
|
+
init_lti_util();
|
|
126466
|
+
init_lti_provisioning();
|
|
126467
|
+
init_params_util();
|
|
126468
|
+
init_secrets_util();
|
|
126469
|
+
init_timeback_util();
|
|
126470
|
+
init_validation_util();
|
|
126471
|
+
});
|
|
125747
126472
|
function buildTimebackClient() {
|
|
125748
126473
|
if (!hasTimebackCredentials()) {
|
|
125749
126474
|
return;
|
|
@@ -126116,7 +126841,7 @@ function createSandboxContext(options) {
|
|
|
126116
126841
|
const services = {};
|
|
126117
126842
|
const ctx = {
|
|
126118
126843
|
db: options.db,
|
|
126119
|
-
|
|
126844
|
+
assessmentRuntimeLock: createInMemoryAssessmentRuntimeLock(),
|
|
126120
126845
|
config: config5,
|
|
126121
126846
|
providers,
|
|
126122
126847
|
services,
|
|
@@ -126145,7 +126870,7 @@ var cachedServiceContext = null;
|
|
|
126145
126870
|
var cachedDb = null;
|
|
126146
126871
|
var init_context = __esm(async () => {
|
|
126147
126872
|
init_config2();
|
|
126148
|
-
|
|
126873
|
+
init_utils7();
|
|
126149
126874
|
init_src6();
|
|
126150
126875
|
init_providers();
|
|
126151
126876
|
await __promiseAll([
|
|
@@ -143454,7 +144179,7 @@ var nullableType2;
|
|
|
143454
144179
|
var preprocessType2;
|
|
143455
144180
|
var pipelineType2;
|
|
143456
144181
|
var coerce2;
|
|
143457
|
-
var
|
|
144182
|
+
var init_types6;
|
|
143458
144183
|
var init_external4;
|
|
143459
144184
|
var init_v32;
|
|
143460
144185
|
var init_esm2;
|
|
@@ -143640,7 +144365,7 @@ var prepareMigrationMeta;
|
|
|
143640
144365
|
var schemaRenameKey;
|
|
143641
144366
|
var tableRenameKey;
|
|
143642
144367
|
var columnRenameKey;
|
|
143643
|
-
var
|
|
144368
|
+
var init_utils8;
|
|
143644
144369
|
var import_hanji;
|
|
143645
144370
|
var warning;
|
|
143646
144371
|
var error88;
|
|
@@ -145179,7 +145904,7 @@ var PrimaryKey3;
|
|
|
145179
145904
|
var init_primary_keys3;
|
|
145180
145905
|
var MySqlViewConfig;
|
|
145181
145906
|
var init_view_common32;
|
|
145182
|
-
var
|
|
145907
|
+
var init_utils82;
|
|
145183
145908
|
var _a3222;
|
|
145184
145909
|
var _b239;
|
|
145185
145910
|
var MySqlDeleteBase;
|
|
@@ -145927,7 +146652,7 @@ var sqlitePushIntrospect = async (db2, filters) => {
|
|
|
145927
146652
|
};
|
|
145928
146653
|
var generateDrizzleJson = (imports, prevId, schemaFilters, casing2) => {
|
|
145929
146654
|
const prepared = prepareFromExports(imports);
|
|
145930
|
-
const id =
|
|
146655
|
+
const id = randomUUID2();
|
|
145931
146656
|
const snapshot = generatePgSnapshot(prepared.tables, prepared.enums, prepared.schemas, prepared.sequences, prepared.roles, prepared.policies, prepared.views, prepared.matViews, casing2, schemaFilters);
|
|
145932
146657
|
return fillPgSnapshot({
|
|
145933
146658
|
serialized: snapshot,
|
|
@@ -145976,7 +146701,7 @@ var pushSchema = async (imports, drizzleInstance, schemaFilters, tablesFilter, e
|
|
|
145976
146701
|
var generateSQLiteDrizzleJson = async (imports, prevId, casing2) => {
|
|
145977
146702
|
const { prepareFromExports: prepareFromExports5 } = await Promise.resolve().then(() => (init_sqliteImports(), sqliteImports_exports));
|
|
145978
146703
|
const prepared = prepareFromExports5(imports);
|
|
145979
|
-
const id =
|
|
146704
|
+
const id = randomUUID2();
|
|
145980
146705
|
const snapshot = generateSqliteSnapshot(prepared.tables, prepared.views, casing2);
|
|
145981
146706
|
return {
|
|
145982
146707
|
...snapshot,
|
|
@@ -146027,7 +146752,7 @@ var pushSQLiteSchema = async (imports, drizzleInstance) => {
|
|
|
146027
146752
|
var generateMySQLDrizzleJson = async (imports, prevId, casing2) => {
|
|
146028
146753
|
const { prepareFromExports: prepareFromExports5 } = await Promise.resolve().then(() => (init_mysqlImports(), mysqlImports_exports));
|
|
146029
146754
|
const prepared = prepareFromExports5(imports);
|
|
146030
|
-
const id =
|
|
146755
|
+
const id = randomUUID2();
|
|
146031
146756
|
const snapshot = generateMySqlSnapshot(prepared.tables, prepared.views, casing2);
|
|
146032
146757
|
return {
|
|
146033
146758
|
...snapshot,
|
|
@@ -146077,7 +146802,7 @@ var pushMySQLSchema = async (imports, drizzleInstance, databaseName) => {
|
|
|
146077
146802
|
var generateSingleStoreDrizzleJson = async (imports, prevId, casing2) => {
|
|
146078
146803
|
const { prepareFromExports: prepareFromExports5 } = await Promise.resolve().then(() => (init_singlestoreImports(), singlestoreImports_exports));
|
|
146079
146804
|
const prepared = prepareFromExports5(imports);
|
|
146080
|
-
const id =
|
|
146805
|
+
const id = randomUUID2();
|
|
146081
146806
|
const snapshot = generateSingleStoreSnapshot(prepared.tables, casing2);
|
|
146082
146807
|
return {
|
|
146083
146808
|
...snapshot,
|
|
@@ -149692,7 +150417,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
|
|
|
149692
150417
|
})(errorUtil2 || (errorUtil2 = {}));
|
|
149693
150418
|
}
|
|
149694
150419
|
});
|
|
149695
|
-
|
|
150420
|
+
init_types6 = __esm2({
|
|
149696
150421
|
"../node_modules/.pnpm/zod@3.25.42/node_modules/zod/dist/esm/v3/types.js"() {
|
|
149697
150422
|
init_ZodError2();
|
|
149698
150423
|
init_errors9();
|
|
@@ -152857,7 +153582,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
|
|
|
152857
153582
|
init_parseUtil2();
|
|
152858
153583
|
init_typeAliases2();
|
|
152859
153584
|
init_util5();
|
|
152860
|
-
|
|
153585
|
+
init_types6();
|
|
152861
153586
|
init_ZodError2();
|
|
152862
153587
|
}
|
|
152863
153588
|
});
|
|
@@ -154590,7 +155315,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
|
|
|
154590
155315
|
backwardCompatibleSqliteSchema = unionType2([sqliteSchemaV5, schema3]);
|
|
154591
155316
|
}
|
|
154592
155317
|
});
|
|
154593
|
-
|
|
155318
|
+
init_utils8 = __esm2({
|
|
154594
155319
|
"src/utils.ts"() {
|
|
154595
155320
|
init_views();
|
|
154596
155321
|
init_global2();
|
|
@@ -154642,7 +155367,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
|
|
|
154642
155367
|
"src/cli/views.ts"() {
|
|
154643
155368
|
init_source();
|
|
154644
155369
|
import_hanji = __toESM5(require_hanji());
|
|
154645
|
-
|
|
155370
|
+
init_utils8();
|
|
154646
155371
|
warning = (msg) => {
|
|
154647
155372
|
(0, import_hanji.render)(`[${source_default.yellow("Warning")}] ${msg}`);
|
|
154648
155373
|
};
|
|
@@ -157350,7 +158075,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
|
|
|
157350
158075
|
init_pgSchema();
|
|
157351
158076
|
init_singlestoreSchema();
|
|
157352
158077
|
init_sqliteSchema();
|
|
157353
|
-
|
|
158078
|
+
init_utils8();
|
|
157354
158079
|
parseType = (schemaPrefix, type) => {
|
|
157355
158080
|
const pgNativeTypes = [
|
|
157356
158081
|
"uuid",
|
|
@@ -159846,7 +160571,7 @@ ${BREAKPOINT}ALTER TABLE ${tableNameWithSchema} ADD CONSTRAINT "${statement.newC
|
|
|
159846
160571
|
init_source();
|
|
159847
160572
|
init_sqliteSchema();
|
|
159848
160573
|
init_sqlgenerator();
|
|
159849
|
-
|
|
160574
|
+
init_utils8();
|
|
159850
160575
|
_moveDataStatements = (tableName, json42, dataLoss = false) => {
|
|
159851
160576
|
const statements = [];
|
|
159852
160577
|
const newTableName = `__new_${tableName}`;
|
|
@@ -162004,7 +162729,7 @@ ${BREAKPOINT}ALTER TABLE ${tableNameWithSchema} ADD CONSTRAINT "${statement.newC
|
|
|
162004
162729
|
init_singlestoreSchema();
|
|
162005
162730
|
init_sqliteSchema();
|
|
162006
162731
|
init_statementCombiner();
|
|
162007
|
-
|
|
162732
|
+
init_utils8();
|
|
162008
162733
|
makeChanged = (schema5) => {
|
|
162009
162734
|
return objectType2({
|
|
162010
162735
|
type: enumType2(["changed"]),
|
|
@@ -164229,7 +164954,7 @@ ${BREAKPOINT}ALTER TABLE ${tableNameWithSchema} ADD CONSTRAINT "${statement.newC
|
|
|
164229
164954
|
init_pgSchema();
|
|
164230
164955
|
init_sqliteSchema();
|
|
164231
164956
|
init_snapshotsDiffer();
|
|
164232
|
-
|
|
164957
|
+
init_utils8();
|
|
164233
164958
|
init_words();
|
|
164234
164959
|
init_outputs();
|
|
164235
164960
|
init_views();
|
|
@@ -170789,7 +171514,7 @@ params: ${params}`);
|
|
|
170789
171514
|
init_pg_core2();
|
|
170790
171515
|
init_vector32();
|
|
170791
171516
|
init_outputs();
|
|
170792
|
-
|
|
171517
|
+
init_utils8();
|
|
170793
171518
|
init_utils62();
|
|
170794
171519
|
indexName = (tableName, columns2) => {
|
|
170795
171520
|
return `${tableName}_${columns2.join("_")}_index`;
|
|
@@ -174798,7 +175523,7 @@ ORDER BY
|
|
|
174798
175523
|
init_dist9();
|
|
174799
175524
|
init_sqlite_core();
|
|
174800
175525
|
init_outputs();
|
|
174801
|
-
|
|
175526
|
+
init_utils8();
|
|
174802
175527
|
init_utils62();
|
|
174803
175528
|
generateSqliteSnapshot = (tables, views, casing2) => {
|
|
174804
175529
|
const dialect6 = new SQLiteSyncDialect({ casing: casing2 });
|
|
@@ -176751,7 +177476,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
176751
177476
|
MySqlViewConfig = Symbol.for("drizzle:MySqlViewConfig");
|
|
176752
177477
|
}
|
|
176753
177478
|
});
|
|
176754
|
-
|
|
177479
|
+
init_utils82 = __esm2({
|
|
176755
177480
|
"../drizzle-orm/dist/mysql-core/utils.js"() {
|
|
176756
177481
|
init_entity2();
|
|
176757
177482
|
init_dist9();
|
|
@@ -176773,7 +177498,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
176773
177498
|
init_query_promise2();
|
|
176774
177499
|
init_selection_proxy2();
|
|
176775
177500
|
init_table8();
|
|
176776
|
-
|
|
177501
|
+
init_utils82();
|
|
176777
177502
|
MySqlDeleteBase = class extends (_b239 = QueryPromise2, _a3222 = entityKind2, _b239) {
|
|
176778
177503
|
constructor(table62, session3, dialect6, withList) {
|
|
176779
177504
|
super();
|
|
@@ -177621,7 +178346,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
177621
178346
|
init_table8();
|
|
177622
178347
|
init_utils22();
|
|
177623
178348
|
init_view_common3();
|
|
177624
|
-
|
|
178349
|
+
init_utils82();
|
|
177625
178350
|
init_view_base3();
|
|
177626
178351
|
_a325 = entityKind2;
|
|
177627
178352
|
MySqlSelectBuilder = class {
|
|
@@ -178020,7 +178745,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
178020
178745
|
init_sql5();
|
|
178021
178746
|
init_table8();
|
|
178022
178747
|
init_utils22();
|
|
178023
|
-
|
|
178748
|
+
init_utils82();
|
|
178024
178749
|
init_query_builder4();
|
|
178025
178750
|
_a329 = entityKind2;
|
|
178026
178751
|
MySqlInsertBuilder = class {
|
|
@@ -178124,7 +178849,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
178124
178849
|
init_selection_proxy2();
|
|
178125
178850
|
init_table8();
|
|
178126
178851
|
init_utils22();
|
|
178127
|
-
|
|
178852
|
+
init_utils82();
|
|
178128
178853
|
_a331 = entityKind2;
|
|
178129
178854
|
MySqlUpdateBuilder = class {
|
|
178130
178855
|
constructor(table62, session3, dialect6, withList) {
|
|
@@ -178660,7 +179385,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
178660
179385
|
init_subquery4();
|
|
178661
179386
|
init_table42();
|
|
178662
179387
|
init_unique_constraint3();
|
|
178663
|
-
|
|
179388
|
+
init_utils82();
|
|
178664
179389
|
init_view_common32();
|
|
178665
179390
|
init_view3();
|
|
178666
179391
|
}
|
|
@@ -178671,7 +179396,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
|
|
|
178671
179396
|
init_dist9();
|
|
178672
179397
|
init_mysql_core();
|
|
178673
179398
|
init_outputs();
|
|
178674
|
-
|
|
179399
|
+
init_utils8();
|
|
178675
179400
|
init_utils62();
|
|
178676
179401
|
handleEnumType = (type) => {
|
|
178677
179402
|
let str = type.split("(")[1];
|
|
@@ -183105,7 +183830,7 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
183105
183830
|
import_hanji10 = __toESM5(require_hanji());
|
|
183106
183831
|
init_sqlgenerator();
|
|
183107
183832
|
init_singlestoreSchema();
|
|
183108
|
-
|
|
183833
|
+
init_utils8();
|
|
183109
183834
|
init_selector_ui();
|
|
183110
183835
|
init_outputs();
|
|
183111
183836
|
filterStatements2 = (statements, currentSchema, prevSchema) => {
|
|
@@ -183368,7 +184093,7 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
183368
184093
|
init_sqlgenerator();
|
|
183369
184094
|
init_selector_ui();
|
|
183370
184095
|
init_pgSchema();
|
|
183371
|
-
|
|
184096
|
+
init_utils8();
|
|
183372
184097
|
import_hanji6 = __toESM5(require_hanji());
|
|
183373
184098
|
init_mjs();
|
|
183374
184099
|
init_global2();
|
|
@@ -184231,323 +184956,6 @@ var init_manifest = __esm(async () => {
|
|
|
184231
184956
|
});
|
|
184232
184957
|
});
|
|
184233
184958
|
});
|
|
184234
|
-
function isAuthenticated(ctx) {
|
|
184235
|
-
return ctx.user != null;
|
|
184236
|
-
}
|
|
184237
|
-
var init_types6 = () => {};
|
|
184238
|
-
function hasGameManagementAccess(user) {
|
|
184239
|
-
return user.role === "admin" || user.role === "teacher" || user.role === "developer" && user.developerStatus === "approved";
|
|
184240
|
-
}
|
|
184241
|
-
function isDashboardWorkerKey(key) {
|
|
184242
|
-
return Boolean(key?.permissions?.[DASHBOARD_PERMISSION_NAMESPACE]);
|
|
184243
|
-
}
|
|
184244
|
-
function workerKeyGrantsGameRead(key, slug2) {
|
|
184245
|
-
if (!isDashboardWorkerKey(key) && !isGameWorkerKey(key)) {
|
|
184246
|
-
return false;
|
|
184247
|
-
}
|
|
184248
|
-
const games2 = key?.permissions?.games;
|
|
184249
|
-
return Boolean(games2?.includes(`read:${slug2}`) || games2?.includes(`write:${slug2}`));
|
|
184250
|
-
}
|
|
184251
|
-
function isGameWorkerKey(key) {
|
|
184252
|
-
return Boolean(key?.name?.startsWith(GAME_WORKER_KEY_PREFIX));
|
|
184253
|
-
}
|
|
184254
|
-
function deniedWorkerKeyRequest(key, method, pathname) {
|
|
184255
|
-
if (isDashboardWorkerKey(key) && !isAllowedDashboardWorkerRequest(method, pathname, key.permissions)) {
|
|
184256
|
-
return DASHBOARD_WORKER_KEY_RESTRICTED;
|
|
184257
|
-
}
|
|
184258
|
-
return null;
|
|
184259
|
-
}
|
|
184260
|
-
function rejectDashboardWorkerKey(ctx) {
|
|
184261
|
-
if (isDashboardWorkerKey(ctx.apiKey)) {
|
|
184262
|
-
throw ApiError.forbidden(DASHBOARD_WORKER_KEY_RESTRICTED);
|
|
184263
|
-
}
|
|
184264
|
-
}
|
|
184265
|
-
function assertAuthenticatedRequest(ctx) {
|
|
184266
|
-
if (!isAuthenticated(ctx)) {
|
|
184267
|
-
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
184268
|
-
}
|
|
184269
|
-
if (ctx.apiKey) {
|
|
184270
|
-
const denied = deniedWorkerKeyRequest(ctx.apiKey, ctx.request.method, ctx.url.pathname);
|
|
184271
|
-
if (denied) {
|
|
184272
|
-
throw ApiError.forbidden(denied);
|
|
184273
|
-
}
|
|
184274
|
-
}
|
|
184275
|
-
}
|
|
184276
|
-
function requireAuth(handler) {
|
|
184277
|
-
return async (ctx) => {
|
|
184278
|
-
assertAuthenticatedRequest(ctx);
|
|
184279
|
-
return handler(ctx);
|
|
184280
|
-
};
|
|
184281
|
-
}
|
|
184282
|
-
function requireNonAnonymous(handler) {
|
|
184283
|
-
return async (ctx) => {
|
|
184284
|
-
assertAuthenticatedRequest(ctx);
|
|
184285
|
-
if (ctx.user.isAnonymous) {
|
|
184286
|
-
throw ApiError.forbidden("This operation is not available for demo/anonymous users");
|
|
184287
|
-
}
|
|
184288
|
-
return handler(ctx);
|
|
184289
|
-
};
|
|
184290
|
-
}
|
|
184291
|
-
function requireAnonymous(handler) {
|
|
184292
|
-
return async (ctx) => {
|
|
184293
|
-
assertAuthenticatedRequest(ctx);
|
|
184294
|
-
if (!ctx.user.isAnonymous) {
|
|
184295
|
-
throw ApiError.forbidden("This operation is only available for demo/anonymous users");
|
|
184296
|
-
}
|
|
184297
|
-
return handler(ctx);
|
|
184298
|
-
};
|
|
184299
|
-
}
|
|
184300
|
-
function requireDeveloper(handler) {
|
|
184301
|
-
return async (ctx) => {
|
|
184302
|
-
assertAuthenticatedRequest(ctx);
|
|
184303
|
-
rejectDashboardWorkerKey(ctx);
|
|
184304
|
-
const isAdmin = ctx.user.role === "admin";
|
|
184305
|
-
const isApprovedDev = ctx.user.role === "developer" && ctx.user.developerStatus === "approved";
|
|
184306
|
-
if (!isAdmin && !isApprovedDev) {
|
|
184307
|
-
throw ApiError.forbidden("Must be an approved developer");
|
|
184308
|
-
}
|
|
184309
|
-
return handler(ctx);
|
|
184310
|
-
};
|
|
184311
|
-
}
|
|
184312
|
-
function requireHumanDeveloper(handler) {
|
|
184313
|
-
return requireDeveloper(async (ctx) => {
|
|
184314
|
-
if (isDashboardWorkerKey(ctx.apiKey) || isGameWorkerKey(ctx.apiKey)) {
|
|
184315
|
-
throw ApiError.forbidden(WORKER_KEY_HUMAN_ONLY);
|
|
184316
|
-
}
|
|
184317
|
-
return handler(ctx);
|
|
184318
|
-
});
|
|
184319
|
-
}
|
|
184320
|
-
function requireGameManagementAccess(handler) {
|
|
184321
|
-
return async (ctx) => {
|
|
184322
|
-
assertAuthenticatedRequest(ctx);
|
|
184323
|
-
rejectDashboardWorkerKey(ctx);
|
|
184324
|
-
if (!hasGameManagementAccess(ctx.user)) {
|
|
184325
|
-
throw ApiError.forbidden("Game management access required");
|
|
184326
|
-
}
|
|
184327
|
-
return handler(ctx);
|
|
184328
|
-
};
|
|
184329
|
-
}
|
|
184330
|
-
var WORKER_KEY_HUMAN_ONLY = "This operation requires your own credential — platform worker keys are refused";
|
|
184331
|
-
var init_auth_util = __esm(() => {
|
|
184332
|
-
init_errors2();
|
|
184333
|
-
init_types6();
|
|
184334
|
-
init_dashboard_util();
|
|
184335
|
-
init_deployment_util();
|
|
184336
|
-
});
|
|
184337
|
-
function defineControllerNames(namespace, handlers) {
|
|
184338
|
-
for (const [key, handler] of Object.entries(handlers)) {
|
|
184339
|
-
Object.defineProperty(handler, "name", {
|
|
184340
|
-
value: `${namespace}.${key}`,
|
|
184341
|
-
configurable: true
|
|
184342
|
-
});
|
|
184343
|
-
}
|
|
184344
|
-
return handlers;
|
|
184345
|
-
}
|
|
184346
|
-
function generateUsername(email5) {
|
|
184347
|
-
const baseUsername = (email5.split("@")[0] || "user").toLowerCase();
|
|
184348
|
-
const cleanUsername = baseUsername.replace(/[^a-z0-9]/g, "");
|
|
184349
|
-
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
184350
|
-
return `${cleanUsername}_${randomSuffix}`;
|
|
184351
|
-
}
|
|
184352
|
-
function extractRedirectPath(targetUri, currentHost) {
|
|
184353
|
-
try {
|
|
184354
|
-
const targetUrl = new URL(targetUri);
|
|
184355
|
-
if (targetUrl.hostname === currentHost) {
|
|
184356
|
-
return targetUrl.pathname + targetUrl.search;
|
|
184357
|
-
}
|
|
184358
|
-
} catch {}
|
|
184359
|
-
return "/";
|
|
184360
|
-
}
|
|
184361
|
-
function validateLtiClaims(claims) {
|
|
184362
|
-
const messageType = claims["https://purl.imsglobal.org/spec/lti/claim/message_type"];
|
|
184363
|
-
const version6 = claims["https://purl.imsglobal.org/spec/lti/claim/version"];
|
|
184364
|
-
if (messageType !== "LtiResourceLinkRequest") {
|
|
184365
|
-
return `Invalid LTI message type: ${messageType}`;
|
|
184366
|
-
}
|
|
184367
|
-
if (version6 !== "1.3.0") {
|
|
184368
|
-
return `Unsupported LTI version: ${version6}`;
|
|
184369
|
-
}
|
|
184370
|
-
return null;
|
|
184371
|
-
}
|
|
184372
|
-
var init_lti_util = () => {};
|
|
184373
|
-
async function provisionLtiUser(db2, claims) {
|
|
184374
|
-
const database = db2;
|
|
184375
|
-
const email5 = claims.email;
|
|
184376
|
-
const ltiTimebackId = claims.sub;
|
|
184377
|
-
const providerId = AUTH_PROVIDER_IDS.TIMEBACK_LTI;
|
|
184378
|
-
if (!email5) {
|
|
184379
|
-
throw new ValidationError("Email is required in LTI claims");
|
|
184380
|
-
}
|
|
184381
|
-
const existingAccount = await database.query.accounts.findFirst({
|
|
184382
|
-
where: and(eq(accounts.accountId, ltiTimebackId), eq(accounts.providerId, providerId))
|
|
184383
|
-
});
|
|
184384
|
-
if (existingAccount) {
|
|
184385
|
-
const user = await database.query.users.findFirst({
|
|
184386
|
-
where: eq(users.id, existingAccount.userId)
|
|
184387
|
-
});
|
|
184388
|
-
if (user) {
|
|
184389
|
-
setAttribute("app.lti.provision", "existing_account");
|
|
184390
|
-
return user;
|
|
184391
|
-
}
|
|
184392
|
-
}
|
|
184393
|
-
const existingUser = await database.query.users.findFirst({
|
|
184394
|
-
where: eq(users.email, email5)
|
|
184395
|
-
});
|
|
184396
|
-
if (existingUser) {
|
|
184397
|
-
await database.transaction(async (tx) => {
|
|
184398
|
-
const existingLtiAccount = await tx.query.accounts.findFirst({
|
|
184399
|
-
where: and(eq(accounts.userId, existingUser.id), eq(accounts.providerId, providerId))
|
|
184400
|
-
});
|
|
184401
|
-
if (!existingLtiAccount) {
|
|
184402
|
-
const [account] = await tx.insert(accounts).values({
|
|
184403
|
-
id: crypto4.randomUUID(),
|
|
184404
|
-
userId: existingUser.id,
|
|
184405
|
-
accountId: ltiTimebackId,
|
|
184406
|
-
providerId,
|
|
184407
|
-
accessToken: null,
|
|
184408
|
-
refreshToken: null,
|
|
184409
|
-
accessTokenExpiresAt: null,
|
|
184410
|
-
refreshTokenExpiresAt: null,
|
|
184411
|
-
createdAt: new Date,
|
|
184412
|
-
updatedAt: new Date
|
|
184413
|
-
}).returning({ id: accounts.id });
|
|
184414
|
-
if (!account) {
|
|
184415
|
-
throw new InternalError("Failed to link LTI account");
|
|
184416
|
-
}
|
|
184417
|
-
}
|
|
184418
|
-
});
|
|
184419
|
-
setAttribute("app.lti.provision", "linked_account");
|
|
184420
|
-
return existingUser;
|
|
184421
|
-
}
|
|
184422
|
-
const newUserId = crypto4.randomUUID();
|
|
184423
|
-
const createdUser = await database.transaction(async (tx) => {
|
|
184424
|
-
const [insertedUser] = await tx.insert(users).values({
|
|
184425
|
-
id: newUserId,
|
|
184426
|
-
email: email5,
|
|
184427
|
-
emailVerified: true,
|
|
184428
|
-
username: generateUsername(email5),
|
|
184429
|
-
name: claims.name || claims.given_name || email5.split("@")[0] || "Timeback User",
|
|
184430
|
-
createdAt: new Date,
|
|
184431
|
-
updatedAt: new Date
|
|
184432
|
-
}).returning();
|
|
184433
|
-
if (!insertedUser) {
|
|
184434
|
-
throw new InternalError("Failed to create user");
|
|
184435
|
-
}
|
|
184436
|
-
await tx.insert(accounts).values({
|
|
184437
|
-
id: crypto4.randomUUID(),
|
|
184438
|
-
userId: newUserId,
|
|
184439
|
-
accountId: ltiTimebackId,
|
|
184440
|
-
providerId,
|
|
184441
|
-
accessToken: null,
|
|
184442
|
-
refreshToken: null,
|
|
184443
|
-
accessTokenExpiresAt: null,
|
|
184444
|
-
refreshTokenExpiresAt: null,
|
|
184445
|
-
createdAt: new Date,
|
|
184446
|
-
updatedAt: new Date
|
|
184447
|
-
});
|
|
184448
|
-
setAttribute("app.lti.provision", "created_user");
|
|
184449
|
-
return insertedUser;
|
|
184450
|
-
});
|
|
184451
|
-
return createdUser;
|
|
184452
|
-
}
|
|
184453
|
-
var init_lti_provisioning = __esm(() => {
|
|
184454
|
-
init_drizzle_orm();
|
|
184455
|
-
init_src();
|
|
184456
|
-
init_tables_index();
|
|
184457
|
-
init_spans();
|
|
184458
|
-
init_errors2();
|
|
184459
|
-
init_lti_util();
|
|
184460
|
-
});
|
|
184461
|
-
function requireGameId(gameId) {
|
|
184462
|
-
if (!gameId) {
|
|
184463
|
-
throw ApiError.badRequest("Missing game ID");
|
|
184464
|
-
}
|
|
184465
|
-
if (!isValidUUID(gameId)) {
|
|
184466
|
-
throw ApiError.unprocessableEntity("gameId must be a valid UUID format");
|
|
184467
|
-
}
|
|
184468
|
-
return gameId;
|
|
184469
|
-
}
|
|
184470
|
-
function requireSlug(slug2) {
|
|
184471
|
-
if (!slug2) {
|
|
184472
|
-
throw ApiError.badRequest("Missing game slug");
|
|
184473
|
-
}
|
|
184474
|
-
return slug2;
|
|
184475
|
-
}
|
|
184476
|
-
function requireUserId(userId) {
|
|
184477
|
-
if (!userId) {
|
|
184478
|
-
throw ApiError.badRequest("Missing user ID");
|
|
184479
|
-
}
|
|
184480
|
-
if (!isValidUUID(userId)) {
|
|
184481
|
-
throw ApiError.unprocessableEntity("userId must be a valid UUID format");
|
|
184482
|
-
}
|
|
184483
|
-
return userId;
|
|
184484
|
-
}
|
|
184485
|
-
function parseLimitParam(url4, max2) {
|
|
184486
|
-
const raw2 = url4.searchParams.get("limit");
|
|
184487
|
-
if (raw2 === null) {
|
|
184488
|
-
return;
|
|
184489
|
-
}
|
|
184490
|
-
const limit = Number(raw2);
|
|
184491
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > max2) {
|
|
184492
|
-
throw ApiError.badRequest(`limit must be an integer between 1 and ${max2}`);
|
|
184493
|
-
}
|
|
184494
|
-
return limit;
|
|
184495
|
-
}
|
|
184496
|
-
var init_params_util = __esm(() => {
|
|
184497
|
-
init_src9();
|
|
184498
|
-
init_errors2();
|
|
184499
|
-
});
|
|
184500
|
-
function formatZodError(error89) {
|
|
184501
|
-
const flat = error89.flatten();
|
|
184502
|
-
const result = {};
|
|
184503
|
-
if (Object.keys(flat.fieldErrors).length > 0) {
|
|
184504
|
-
result.fields = {};
|
|
184505
|
-
for (const [field, messages] of Object.entries(flat.fieldErrors)) {
|
|
184506
|
-
if (messages && messages.length > 0) {
|
|
184507
|
-
result.fields[field] = messages[0];
|
|
184508
|
-
}
|
|
184509
|
-
}
|
|
184510
|
-
}
|
|
184511
|
-
if (flat.formErrors.length > 0) {
|
|
184512
|
-
result.errors = flat.formErrors;
|
|
184513
|
-
}
|
|
184514
|
-
return result;
|
|
184515
|
-
}
|
|
184516
|
-
function recordValidationFailure(details) {
|
|
184517
|
-
setAttribute("app.request.outcome", "validation_failed");
|
|
184518
|
-
addEvent("request.validation_failed", {
|
|
184519
|
-
"app.validation.error": JSON.stringify(details)
|
|
184520
|
-
});
|
|
184521
|
-
}
|
|
184522
|
-
async function parseRequestBody(request, schema4) {
|
|
184523
|
-
try {
|
|
184524
|
-
return schema4.parse(await request.json());
|
|
184525
|
-
} catch (error89) {
|
|
184526
|
-
if (error89 instanceof exports_external.ZodError) {
|
|
184527
|
-
const details = formatZodError(error89);
|
|
184528
|
-
recordValidationFailure(details);
|
|
184529
|
-
throw ApiError.unprocessableEntity("Validation failed", details);
|
|
184530
|
-
}
|
|
184531
|
-
throw ApiError.invalidJsonBody();
|
|
184532
|
-
}
|
|
184533
|
-
}
|
|
184534
|
-
var init_validation_util = __esm(() => {
|
|
184535
|
-
init_esm();
|
|
184536
|
-
init_spans();
|
|
184537
|
-
init_errors2();
|
|
184538
|
-
});
|
|
184539
|
-
var init_utils11 = __esm(() => {
|
|
184540
|
-
init_auth_util();
|
|
184541
|
-
init_dashboard_util();
|
|
184542
|
-
init_deployment_util();
|
|
184543
|
-
init_leaderboard_util();
|
|
184544
|
-
init_lti_util();
|
|
184545
|
-
init_lti_provisioning();
|
|
184546
|
-
init_params_util();
|
|
184547
|
-
init_secrets_util();
|
|
184548
|
-
init_timeback_util();
|
|
184549
|
-
init_validation_util();
|
|
184550
|
-
});
|
|
184551
184959
|
var listFiles;
|
|
184552
184960
|
var getFile;
|
|
184553
184961
|
var putFile;
|
|
@@ -184556,7 +184964,7 @@ var initiateUpload;
|
|
|
184556
184964
|
var bucket;
|
|
184557
184965
|
var init_bucket_controller = __esm(() => {
|
|
184558
184966
|
init_errors2();
|
|
184559
|
-
|
|
184967
|
+
init_utils7();
|
|
184560
184968
|
listFiles = requireDeveloper(async (ctx) => {
|
|
184561
184969
|
const slug2 = ctx.params.slug;
|
|
184562
184970
|
if (!slug2) {
|
|
@@ -184655,7 +185063,7 @@ var dashboard2;
|
|
|
184655
185063
|
var init_dashboard_controller = __esm(() => {
|
|
184656
185064
|
init_schemas_index();
|
|
184657
185065
|
init_errors2();
|
|
184658
|
-
|
|
185066
|
+
init_utils7();
|
|
184659
185067
|
listUsers = requireHumanDeveloper(async (ctx) => {
|
|
184660
185068
|
const slug2 = requireSlug(ctx.params.slug);
|
|
184661
185069
|
const users2 = await ctx.services.dashboard.listUsers(slug2, ctx.user);
|
|
@@ -184726,7 +185134,7 @@ var init_database_controller = __esm(() => {
|
|
|
184726
185134
|
init_esm();
|
|
184727
185135
|
init_schemas_index();
|
|
184728
185136
|
init_errors2();
|
|
184729
|
-
|
|
185137
|
+
init_utils7();
|
|
184730
185138
|
reset = requireDeveloper(async (ctx) => {
|
|
184731
185139
|
const slug2 = ctx.params.slug;
|
|
184732
185140
|
if (!slug2) {
|
|
@@ -184787,7 +185195,7 @@ var init_deploy_controller = __esm(() => {
|
|
|
184787
185195
|
init_esm();
|
|
184788
185196
|
init_schemas_index();
|
|
184789
185197
|
init_errors2();
|
|
184790
|
-
|
|
185198
|
+
init_utils7();
|
|
184791
185199
|
deploy = defineControllerNames("deploy", {
|
|
184792
185200
|
createJob: requireDeveloper(createJob),
|
|
184793
185201
|
getJob: requireDeveloper(getJob)
|
|
@@ -184805,7 +185213,7 @@ var deploymentState;
|
|
|
184805
185213
|
var init_deployment_state_controller = __esm(() => {
|
|
184806
185214
|
init_schemas_index();
|
|
184807
185215
|
init_errors2();
|
|
184808
|
-
|
|
185216
|
+
init_utils7();
|
|
184809
185217
|
get = requireDeveloper(async (ctx) => {
|
|
184810
185218
|
const slug2 = requireSlug(ctx.params.slug);
|
|
184811
185219
|
const include = ctx.url.searchParams.getAll("include").flatMap((value) => value.split(",")).filter(Boolean);
|
|
@@ -184875,7 +185283,7 @@ var apply;
|
|
|
184875
185283
|
var getStatus;
|
|
184876
185284
|
var developer;
|
|
184877
185285
|
var init_developer_controller = __esm(() => {
|
|
184878
|
-
|
|
185286
|
+
init_utils7();
|
|
184879
185287
|
apply = requireNonAnonymous(async (ctx) => {
|
|
184880
185288
|
await ctx.services.developer.apply(ctx.user);
|
|
184881
185289
|
});
|
|
@@ -184898,7 +185306,7 @@ var init_domain_controller = __esm(() => {
|
|
|
184898
185306
|
init_schemas_index();
|
|
184899
185307
|
init_config2();
|
|
184900
185308
|
init_errors2();
|
|
184901
|
-
|
|
185309
|
+
init_utils7();
|
|
184902
185310
|
add = requireDeveloper(async (ctx) => {
|
|
184903
185311
|
const slug2 = ctx.params.slug;
|
|
184904
185312
|
if (!slug2) {
|
|
@@ -184975,7 +185383,7 @@ var gameMembers2;
|
|
|
184975
185383
|
var init_game_member_controller = __esm(() => {
|
|
184976
185384
|
init_schemas_index();
|
|
184977
185385
|
init_errors2();
|
|
184978
|
-
|
|
185386
|
+
init_utils7();
|
|
184979
185387
|
listMembers = requireNonAnonymous(async (ctx) => {
|
|
184980
185388
|
const gameId = requireGameId(ctx.params.gameId);
|
|
184981
185389
|
return ctx.services.gameMember.list(gameId, ctx.user);
|
|
@@ -185024,7 +185432,7 @@ var init_game_controller = __esm(() => {
|
|
|
185024
185432
|
init_esm();
|
|
185025
185433
|
init_schemas_index();
|
|
185026
185434
|
init_errors2();
|
|
185027
|
-
|
|
185435
|
+
init_utils7();
|
|
185028
185436
|
init_auth_util();
|
|
185029
185437
|
list2 = requireNonAnonymous(async (ctx) => ctx.services.game.list(ctx.user));
|
|
185030
185438
|
listAccessible = requireNonAnonymous(async (ctx) => ctx.services.game.listAccessible(ctx.user));
|
|
@@ -185101,7 +185509,7 @@ var init_kv_controller = __esm(() => {
|
|
|
185101
185509
|
init_esm();
|
|
185102
185510
|
init_schemas_index();
|
|
185103
185511
|
init_errors2();
|
|
185104
|
-
|
|
185512
|
+
init_utils7();
|
|
185105
185513
|
listKeys = requireDeveloper(async (ctx) => {
|
|
185106
185514
|
const slug2 = ctx.params.slug;
|
|
185107
185515
|
if (!slug2) {
|
|
@@ -185209,7 +185617,7 @@ var init_leaderboard_controller = __esm(() => {
|
|
|
185209
185617
|
init_esm();
|
|
185210
185618
|
init_schemas_index();
|
|
185211
185619
|
init_errors2();
|
|
185212
|
-
|
|
185620
|
+
init_utils7();
|
|
185213
185621
|
submitScore = requireAuth(async (ctx) => {
|
|
185214
185622
|
const gameId = ctx.params.gameId;
|
|
185215
185623
|
if (!gameId) {
|
|
@@ -185317,7 +185725,7 @@ var generateToken;
|
|
|
185317
185725
|
var logs;
|
|
185318
185726
|
var init_logs_controller = __esm(() => {
|
|
185319
185727
|
init_errors2();
|
|
185320
|
-
|
|
185728
|
+
init_utils7();
|
|
185321
185729
|
generateToken = requireDeveloper(async (ctx) => {
|
|
185322
185730
|
const slug2 = ctx.params.slug;
|
|
185323
185731
|
if (!slug2) {
|
|
@@ -185351,7 +185759,7 @@ var init_logs_controller = __esm(() => {
|
|
|
185351
185759
|
var getStatus3;
|
|
185352
185760
|
var lti;
|
|
185353
185761
|
var init_lti_controller = __esm(() => {
|
|
185354
|
-
|
|
185762
|
+
init_utils7();
|
|
185355
185763
|
getStatus3 = requireNonAnonymous(async (ctx) => ctx.services.lti.getStatus(ctx.user));
|
|
185356
185764
|
lti = defineControllerNames("lti", {
|
|
185357
185765
|
getStatus: getStatus3
|
|
@@ -185366,7 +185774,7 @@ var init_secrets_controller = __esm(() => {
|
|
|
185366
185774
|
init_esm();
|
|
185367
185775
|
init_schemas_index();
|
|
185368
185776
|
init_errors2();
|
|
185369
|
-
|
|
185777
|
+
init_utils7();
|
|
185370
185778
|
listKeys2 = requireDeveloper(async (ctx) => {
|
|
185371
185779
|
const slug2 = ctx.params.slug;
|
|
185372
185780
|
if (!slug2) {
|
|
@@ -185428,7 +185836,7 @@ var init_seed_controller = __esm(() => {
|
|
|
185428
185836
|
init_esm();
|
|
185429
185837
|
init_schemas_index();
|
|
185430
185838
|
init_errors2();
|
|
185431
|
-
|
|
185839
|
+
init_utils7();
|
|
185432
185840
|
seed2 = requireDeveloper(async (ctx) => {
|
|
185433
185841
|
const slug2 = ctx.params.slug;
|
|
185434
185842
|
if (!slug2) {
|
|
@@ -185460,7 +185868,7 @@ var init_session_controller = __esm(() => {
|
|
|
185460
185868
|
init_spans();
|
|
185461
185869
|
init_tunnel();
|
|
185462
185870
|
init_errors2();
|
|
185463
|
-
|
|
185871
|
+
init_utils7();
|
|
185464
185872
|
MintTokenBody = exports_external.object({
|
|
185465
185873
|
reason: exports_external.enum(["initial", "refresh_scheduled", "refresh_stale"]).optional()
|
|
185466
185874
|
});
|
|
@@ -185557,6 +185965,7 @@ var listAssessments;
|
|
|
185557
185965
|
var createAssessment;
|
|
185558
185966
|
var attachExistingAssessments;
|
|
185559
185967
|
var updateAssessment;
|
|
185968
|
+
var updateReviewMapping;
|
|
185560
185969
|
var reorderAssessments;
|
|
185561
185970
|
var reorderQuestions;
|
|
185562
185971
|
var removeAssessment;
|
|
@@ -185576,7 +185985,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
185576
185985
|
init_src9();
|
|
185577
185986
|
init_timeback3();
|
|
185578
185987
|
init_errors2();
|
|
185579
|
-
|
|
185988
|
+
init_utils7();
|
|
185580
185989
|
populateStudent = requireNonAnonymous(async (ctx) => {
|
|
185581
185990
|
let providedNames;
|
|
185582
185991
|
try {
|
|
@@ -186241,6 +186650,14 @@ var init_timeback_controller = __esm(() => {
|
|
|
186241
186650
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
186242
186651
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
186243
186652
|
});
|
|
186653
|
+
updateReviewMapping = requireDeveloper(async (ctx) => {
|
|
186654
|
+
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
186655
|
+
if (!gameId || !courseId || !testIdentifier) {
|
|
186656
|
+
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
186657
|
+
}
|
|
186658
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
186659
|
+
return ctx.services.timebackAssessments.updateReviewMapping(integrationId, testIdentifier);
|
|
186660
|
+
});
|
|
186244
186661
|
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
186245
186662
|
const { gameId, courseId } = ctx.params;
|
|
186246
186663
|
if (!gameId || !courseId) {
|
|
@@ -186383,6 +186800,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
186383
186800
|
createAssessment,
|
|
186384
186801
|
attachExistingAssessments,
|
|
186385
186802
|
updateAssessment,
|
|
186803
|
+
updateReviewMapping,
|
|
186386
186804
|
reorderAssessments,
|
|
186387
186805
|
removeAssessment,
|
|
186388
186806
|
reorderQuestions,
|
|
@@ -186401,7 +186819,7 @@ var init_upload_controller = __esm(() => {
|
|
|
186401
186819
|
init_esm();
|
|
186402
186820
|
init_schemas_index();
|
|
186403
186821
|
init_errors2();
|
|
186404
|
-
|
|
186822
|
+
init_utils7();
|
|
186405
186823
|
initiate = requireDeveloper(async (ctx) => {
|
|
186406
186824
|
let body2;
|
|
186407
186825
|
try {
|
|
@@ -186428,7 +186846,7 @@ var users2;
|
|
|
186428
186846
|
var init_user_controller = __esm(() => {
|
|
186429
186847
|
init_src();
|
|
186430
186848
|
init_schemas_index();
|
|
186431
|
-
|
|
186849
|
+
init_utils7();
|
|
186432
186850
|
getMe = requireNonAnonymous(async (ctx) => ctx.services.user.getMe(ctx.user, ctx.gameId, ctx.request.headers.get(PLAYCADEMY_BROWSER_TIME_ZONE_HEADER)));
|
|
186433
186851
|
getDemoProfile = requireAnonymous(async (ctx) => ctx.services.user.getDemoProfile(ctx.user.id));
|
|
186434
186852
|
updateDemoProfile = requireAnonymous(async (ctx) => {
|
|
@@ -186461,7 +186879,7 @@ var init_verify_controller = __esm(() => {
|
|
|
186461
186879
|
init_esm();
|
|
186462
186880
|
init_schemas_index();
|
|
186463
186881
|
init_errors2();
|
|
186464
|
-
|
|
186882
|
+
init_utils7();
|
|
186465
186883
|
verify = defineControllerNames("verify", {
|
|
186466
186884
|
verifyToken
|
|
186467
186885
|
});
|
|
@@ -186642,7 +187060,7 @@ async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
|
186642
187060
|
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS2;
|
|
186643
187061
|
var init_timeback7 = __esm(() => {
|
|
186644
187062
|
init_drizzle_orm();
|
|
186645
|
-
|
|
187063
|
+
init_utils7();
|
|
186646
187064
|
init_src();
|
|
186647
187065
|
init_tables_index();
|
|
186648
187066
|
init_src6();
|
|
@@ -187053,7 +187471,7 @@ var init_seed2 = __esm(async () => {
|
|
|
187053
187471
|
init_dist7();
|
|
187054
187472
|
init_esm();
|
|
187055
187473
|
init_errors2();
|
|
187056
|
-
|
|
187474
|
+
init_utils7();
|
|
187057
187475
|
init_schemas_index();
|
|
187058
187476
|
await init_api3();
|
|
187059
187477
|
gameSeedRouter = new Hono2;
|
|
@@ -187283,7 +187701,7 @@ var init_timeback8 = __esm(async () => {
|
|
|
187283
187701
|
init_dist7();
|
|
187284
187702
|
init_controllers();
|
|
187285
187703
|
init_errors2();
|
|
187286
|
-
|
|
187704
|
+
init_utils7();
|
|
187287
187705
|
init_src();
|
|
187288
187706
|
init_schemas_index();
|
|
187289
187707
|
init_timeback3();
|
|
@@ -187522,7 +187940,7 @@ var init_lti = __esm(async () => {
|
|
|
187522
187940
|
init_drizzle_orm();
|
|
187523
187941
|
init_dist7();
|
|
187524
187942
|
init_controllers();
|
|
187525
|
-
|
|
187943
|
+
init_utils7();
|
|
187526
187944
|
init_tables_index();
|
|
187527
187945
|
init_src6();
|
|
187528
187946
|
init_constants();
|