@playcademy/sandbox 0.12.2-beta.1 → 0.12.2-beta.3
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/cli.js +640 -194
- package/dist/server.js +640 -194
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -697,7 +697,7 @@ var package_default;
|
|
|
697
697
|
var init_package = __esm(() => {
|
|
698
698
|
package_default = {
|
|
699
699
|
name: "@playcademy/sandbox",
|
|
700
|
-
version: "0.12.2-beta.
|
|
700
|
+
version: "0.12.2-beta.3",
|
|
701
701
|
description: "Local development server for Playcademy game development",
|
|
702
702
|
type: "module",
|
|
703
703
|
exports: {
|
|
@@ -12954,6 +12954,7 @@ var init_schema = __esm(() => {
|
|
|
12954
12954
|
assessmentArtifactBucket: stringType().optional(),
|
|
12955
12955
|
cdnOrigin: stringType().url().startsWith("https://").optional(),
|
|
12956
12956
|
queueIngressSecret: stringType().optional(),
|
|
12957
|
+
embedAdminToken: stringType().optional(),
|
|
12957
12958
|
secretsManifestPepper: stringType().optional()
|
|
12958
12959
|
}).superRefine((config, ctx) => {
|
|
12959
12960
|
if (config.isLocal && !config.baseUrl) {
|
|
@@ -13120,6 +13121,17 @@ var init_table_utils = __esm(() => {
|
|
|
13120
13121
|
});
|
|
13121
13122
|
|
|
13122
13123
|
// ../../node_modules/.bun/drizzle-orm@0.45.3+c063d24bf874cbdc/node_modules/drizzle-orm/pg-core/foreign-keys.js
|
|
13124
|
+
function foreignKey(config) {
|
|
13125
|
+
function mappedConfig() {
|
|
13126
|
+
const { name: name2, columns, foreignColumns } = config;
|
|
13127
|
+
return {
|
|
13128
|
+
name: name2,
|
|
13129
|
+
columns,
|
|
13130
|
+
foreignColumns
|
|
13131
|
+
};
|
|
13132
|
+
}
|
|
13133
|
+
return new ForeignKeyBuilder(mappedConfig);
|
|
13134
|
+
}
|
|
13123
13135
|
var ForeignKeyBuilder, ForeignKey;
|
|
13124
13136
|
var init_foreign_keys = __esm(() => {
|
|
13125
13137
|
init_entity();
|
|
@@ -15992,6 +16004,16 @@ function ilike(column, value) {
|
|
|
15992
16004
|
function notIlike(column, value) {
|
|
15993
16005
|
return sql`${column} not ilike ${value}`;
|
|
15994
16006
|
}
|
|
16007
|
+
function arrayContains(column, values) {
|
|
16008
|
+
if (Array.isArray(values)) {
|
|
16009
|
+
if (values.length === 0) {
|
|
16010
|
+
throw new Error("arrayContains requires at least one value");
|
|
16011
|
+
}
|
|
16012
|
+
const array = sql`${bindIfParam(values, column)}`;
|
|
16013
|
+
return sql`${column} @> ${array}`;
|
|
16014
|
+
}
|
|
16015
|
+
return sql`${column} @> ${bindIfParam(values, column)}`;
|
|
16016
|
+
}
|
|
15995
16017
|
var eq = (left, right) => {
|
|
15996
16018
|
return sql`${left} = ${bindIfParam(right, left)}`;
|
|
15997
16019
|
}, ne = (left, right) => {
|
|
@@ -18922,7 +18944,7 @@ var init_table6 = __esm(() => {
|
|
|
18922
18944
|
});
|
|
18923
18945
|
|
|
18924
18946
|
// ../data/src/domains/timeback/table.ts
|
|
18925
|
-
var gameTimebackIntegrationStatusEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
18947
|
+
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrationKindEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
18926
18948
|
var init_table7 = __esm(() => {
|
|
18927
18949
|
init_drizzle_orm();
|
|
18928
18950
|
init_pg_core();
|
|
@@ -18933,6 +18955,10 @@ var init_table7 = __esm(() => {
|
|
|
18933
18955
|
"active",
|
|
18934
18956
|
"deactivated"
|
|
18935
18957
|
]);
|
|
18958
|
+
gameTimebackIntegrationKindEnum = pgEnum("game_timeback_integration_kind", [
|
|
18959
|
+
"base",
|
|
18960
|
+
"hole_filling"
|
|
18961
|
+
]);
|
|
18936
18962
|
gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", ASSESSMENT_PURPOSES);
|
|
18937
18963
|
gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
|
|
18938
18964
|
"draft",
|
|
@@ -18946,6 +18972,8 @@ var init_table7 = __esm(() => {
|
|
|
18946
18972
|
grade: integer("grade").notNull(),
|
|
18947
18973
|
subject: text("subject").notNull(),
|
|
18948
18974
|
status: gameTimebackIntegrationStatusEnum("status"),
|
|
18975
|
+
kind: gameTimebackIntegrationKindEnum("kind").notNull().default("base"),
|
|
18976
|
+
baseIntegrationId: uuid("base_integration_id"),
|
|
18949
18977
|
totalXp: integer("total_xp"),
|
|
18950
18978
|
lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
|
|
18951
18979
|
deactivatedAt: timestamp("deactivated_at", { withTimezone: true }),
|
|
@@ -18953,7 +18981,19 @@ var init_table7 = __esm(() => {
|
|
|
18953
18981
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
18954
18982
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
18955
18983
|
}, (table) => [
|
|
18956
|
-
uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table.gameId, table.grade, table.subject).where(sql`${table.status} IS DISTINCT FROM 'deactivated'`)
|
|
18984
|
+
uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table.gameId, table.grade, table.subject).where(sql`${table.status} IS DISTINCT FROM 'deactivated' AND ${table.kind} = 'base'`),
|
|
18985
|
+
uniqueIndex("game_timeback_integrations_hole_filling_course_idx").on(table.gameId, table.courseId).where(sql`${table.kind} = 'hole_filling'`),
|
|
18986
|
+
index("game_timeback_integrations_base_integration_idx").on(table.baseIntegrationId),
|
|
18987
|
+
foreignKey({
|
|
18988
|
+
name: "game_timeback_integrations_base_integration_id_fk",
|
|
18989
|
+
columns: [table.baseIntegrationId],
|
|
18990
|
+
foreignColumns: [table.id]
|
|
18991
|
+
}),
|
|
18992
|
+
check("game_timeback_integrations_kind_base_check", sql`(
|
|
18993
|
+
(${table.kind}::text = 'base' AND ${table.baseIntegrationId} IS NULL)
|
|
18994
|
+
OR
|
|
18995
|
+
(${table.kind}::text = 'hole_filling' AND ${table.baseIntegrationId} IS NOT NULL)
|
|
18996
|
+
)`)
|
|
18957
18997
|
]);
|
|
18958
18998
|
gameTimebackAssessmentTests = pgTable("game_timeback_assessment_tests", {
|
|
18959
18999
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -19035,7 +19075,7 @@ var init_table7 = __esm(() => {
|
|
|
19035
19075
|
});
|
|
19036
19076
|
|
|
19037
19077
|
// ../data/src/domains/embed/table.ts
|
|
19038
|
-
var embedPartners,
|
|
19078
|
+
var embedPartners, embedLaunches, embedLaunchesRelations;
|
|
19039
19079
|
var init_table8 = __esm(() => {
|
|
19040
19080
|
init_drizzle_orm();
|
|
19041
19081
|
init_pg_core();
|
|
@@ -19050,15 +19090,6 @@ var init_table8 = __esm(() => {
|
|
|
19050
19090
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
19051
19091
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
19052
19092
|
});
|
|
19053
|
-
embedPartnerGames = pgTable("embed_partner_games", {
|
|
19054
|
-
id: uuid("id").primaryKey().defaultRandom(),
|
|
19055
|
-
partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "cascade" }),
|
|
19056
|
-
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
19057
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
19058
|
-
}, (table) => [
|
|
19059
|
-
uniqueIndex("embed_partner_games_partner_game_idx").on(table.partnerId, table.gameId),
|
|
19060
|
-
index("embed_partner_games_game_idx").on(table.gameId)
|
|
19061
|
-
]);
|
|
19062
19093
|
embedLaunches = pgTable("embed_launches", {
|
|
19063
19094
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
19064
19095
|
partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
|
|
@@ -19077,19 +19108,6 @@ var init_table8 = __esm(() => {
|
|
|
19077
19108
|
index("embed_launches_user_idx").on(table.userId),
|
|
19078
19109
|
index("embed_launches_game_idx").on(table.gameId)
|
|
19079
19110
|
]);
|
|
19080
|
-
embedPartnersRelations = relations(embedPartners, ({ many }) => ({
|
|
19081
|
-
games: many(embedPartnerGames)
|
|
19082
|
-
}));
|
|
19083
|
-
embedPartnerGamesRelations = relations(embedPartnerGames, ({ one }) => ({
|
|
19084
|
-
partner: one(embedPartners, {
|
|
19085
|
-
fields: [embedPartnerGames.partnerId],
|
|
19086
|
-
references: [embedPartners.id]
|
|
19087
|
-
}),
|
|
19088
|
-
game: one(games, {
|
|
19089
|
-
fields: [embedPartnerGames.gameId],
|
|
19090
|
-
references: [games.id]
|
|
19091
|
-
})
|
|
19092
|
-
}));
|
|
19093
19111
|
embedLaunchesRelations = relations(embedLaunches, ({ one }) => ({
|
|
19094
19112
|
partner: one(embedPartners, {
|
|
19095
19113
|
fields: [embedLaunches.partnerId],
|
|
@@ -19120,10 +19138,7 @@ __export(exports_tables_index, {
|
|
|
19120
19138
|
developerStatusEnum: () => developerStatusEnum,
|
|
19121
19139
|
embedLaunches: () => embedLaunches,
|
|
19122
19140
|
embedLaunchesRelations: () => embedLaunchesRelations,
|
|
19123
|
-
embedPartnerGames: () => embedPartnerGames,
|
|
19124
|
-
embedPartnerGamesRelations: () => embedPartnerGamesRelations,
|
|
19125
19141
|
embedPartners: () => embedPartners,
|
|
19126
|
-
embedPartnersRelations: () => embedPartnersRelations,
|
|
19127
19142
|
gameCustomHostnames: () => gameCustomHostnames,
|
|
19128
19143
|
gameDashboardUserRoleEnum: () => gameDashboardUserRoleEnum,
|
|
19129
19144
|
gameDashboardUsers: () => gameDashboardUsers,
|
|
@@ -19142,6 +19157,7 @@ __export(exports_tables_index, {
|
|
|
19142
19157
|
gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
|
|
19143
19158
|
gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
|
|
19144
19159
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
19160
|
+
gameTimebackIntegrationKindEnum: () => gameTimebackIntegrationKindEnum,
|
|
19145
19161
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
19146
19162
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
19147
19163
|
gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
|
|
@@ -21049,10 +21065,19 @@ var init_helpers = __esm(() => {
|
|
|
21049
21065
|
});
|
|
21050
21066
|
|
|
21051
21067
|
// ../data/src/domains/timeback/helpers.ts
|
|
21068
|
+
function isBaseGameTimebackIntegration() {
|
|
21069
|
+
return sql`${gameTimebackIntegrations.kind} = ${BASE_GAME_TIMEBACK_INTEGRATION_KIND}`;
|
|
21070
|
+
}
|
|
21071
|
+
function isBaseGameTimebackIntegrationRow(integration) {
|
|
21072
|
+
return integration.kind === BASE_GAME_TIMEBACK_INTEGRATION_KIND;
|
|
21073
|
+
}
|
|
21052
21074
|
function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
|
|
21053
21075
|
return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
|
|
21054
21076
|
}
|
|
21055
|
-
|
|
21077
|
+
function isActiveBaseGameTimebackIntegration() {
|
|
21078
|
+
return sql`(${isActiveGameTimebackIntegrationStatus()} AND ${isBaseGameTimebackIntegration()})`;
|
|
21079
|
+
}
|
|
21080
|
+
var ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS = "active", DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS = "deactivated", BASE_GAME_TIMEBACK_INTEGRATION_KIND = "base", HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND = "hole_filling";
|
|
21056
21081
|
var init_helpers2 = __esm(() => {
|
|
21057
21082
|
init_drizzle_orm();
|
|
21058
21083
|
init_table7();
|
|
@@ -37629,12 +37654,12 @@ function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
|
|
|
37629
37654
|
function filterEnrollmentsByGame(enrollments, gameId) {
|
|
37630
37655
|
return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _, ...enrollment }) => enrollment);
|
|
37631
37656
|
}
|
|
37632
|
-
function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
37657
|
+
function mapEnrollmentsToUserEnrollments(enrollments, integrations, baseCourseIdByIntegrationId = new Map) {
|
|
37633
37658
|
const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
|
|
37634
37659
|
const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
|
|
37635
37660
|
return integrations.map((integration) => {
|
|
37636
37661
|
const enrollment = enrollmentByCourse.get(integration.courseId);
|
|
37637
|
-
|
|
37662
|
+
const userEnrollment = {
|
|
37638
37663
|
gameId: integration.gameId,
|
|
37639
37664
|
grade: integration.grade,
|
|
37640
37665
|
subject: integration.subject,
|
|
@@ -37642,6 +37667,17 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
37642
37667
|
orgId: courseToSchool.get(integration.courseId),
|
|
37643
37668
|
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
37644
37669
|
};
|
|
37670
|
+
if (integration.kind !== HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
37671
|
+
return userEnrollment;
|
|
37672
|
+
}
|
|
37673
|
+
const baseCourseId = integration.baseIntegrationId ? baseCourseIdByIntegrationId.get(integration.baseIntegrationId) : undefined;
|
|
37674
|
+
return {
|
|
37675
|
+
...userEnrollment,
|
|
37676
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
37677
|
+
...baseCourseId && { baseCourseId },
|
|
37678
|
+
...enrollment?.beginDate !== undefined && { beginDate: enrollment.beginDate },
|
|
37679
|
+
...enrollment?.metadata && { enrollmentMetadata: enrollment.metadata }
|
|
37680
|
+
};
|
|
37645
37681
|
});
|
|
37646
37682
|
}
|
|
37647
37683
|
function buildGameTimebackSummaries(integrations) {
|
|
@@ -38124,7 +38160,7 @@ var init_game_service = __esm(() => {
|
|
|
38124
38160
|
const db = this.deps.db;
|
|
38125
38161
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
38126
38162
|
columns: { gameId: true, subject: true },
|
|
38127
|
-
where:
|
|
38163
|
+
where: isActiveBaseGameTimebackIntegration(),
|
|
38128
38164
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
38129
38165
|
});
|
|
38130
38166
|
const subjectMap = {};
|
|
@@ -38148,9 +38184,7 @@ var init_game_service = __esm(() => {
|
|
|
38148
38184
|
}
|
|
38149
38185
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
38150
38186
|
columns: { gameId: true, subject: true, status: true },
|
|
38151
|
-
...accessibleGameIds
|
|
38152
|
-
where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
|
|
38153
|
-
},
|
|
38187
|
+
where: and(accessibleGameIds ? inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds]) : undefined, isBaseGameTimebackIntegration()),
|
|
38154
38188
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
38155
38189
|
});
|
|
38156
38190
|
return buildGameTimebackSummaries(integrations);
|
|
@@ -48581,7 +48615,7 @@ function rejectSystemManagedReview(input, context) {
|
|
|
48581
48615
|
});
|
|
48582
48616
|
}
|
|
48583
48617
|
}
|
|
48584
|
-
var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
48618
|
+
var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, RESERVED_TIMEBACK_COURSE_SOURCED_IDS, TimebackCourseSourcedIdSchema, CreateGameTimebackHoleFillingCourseRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
48585
48619
|
var init_schemas4 = __esm(() => {
|
|
48586
48620
|
init_esm();
|
|
48587
48621
|
init_src();
|
|
@@ -48617,6 +48651,26 @@ var init_schemas4 = __esm(() => {
|
|
|
48617
48651
|
masterableUnits: numberType().int().nonnegative(),
|
|
48618
48652
|
level: stringType().trim().min(1).optional()
|
|
48619
48653
|
});
|
|
48654
|
+
RESERVED_TIMEBACK_COURSE_SOURCED_IDS = new Set([
|
|
48655
|
+
"hole-filling",
|
|
48656
|
+
"removed",
|
|
48657
|
+
"components",
|
|
48658
|
+
"component-resources",
|
|
48659
|
+
"structure"
|
|
48660
|
+
]);
|
|
48661
|
+
TimebackCourseSourcedIdSchema = stringType().trim().min(3).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
|
|
48662
|
+
message: "Use letters, numbers, dots, dashes, or underscores, starting with a letter or number"
|
|
48663
|
+
}).refine((value) => !RESERVED_TIMEBACK_COURSE_SOURCED_IDS.has(value.toLowerCase()), {
|
|
48664
|
+
message: "This course ID is reserved"
|
|
48665
|
+
});
|
|
48666
|
+
CreateGameTimebackHoleFillingCourseRequestSchema = objectType({
|
|
48667
|
+
baseCourseId: stringType().trim().min(1),
|
|
48668
|
+
title: stringType().trim().min(1),
|
|
48669
|
+
courseCode: stringType().trim().min(1),
|
|
48670
|
+
totalXp: numberType().int().positive(),
|
|
48671
|
+
masterableUnits: numberType().int().nonnegative(),
|
|
48672
|
+
courseId: TimebackCourseSourcedIdSchema.optional()
|
|
48673
|
+
});
|
|
48620
48674
|
TimebackActivityDataSchema = objectType({
|
|
48621
48675
|
activityId: stringType().min(1),
|
|
48622
48676
|
activityName: stringType().optional(),
|
|
@@ -48625,6 +48679,7 @@ var init_schemas4 = __esm(() => {
|
|
|
48625
48679
|
appName: stringType().optional(),
|
|
48626
48680
|
sensorUrl: stringType().url().optional(),
|
|
48627
48681
|
courseId: stringType().min(1).optional(),
|
|
48682
|
+
enrollmentId: stringType().min(1).optional(),
|
|
48628
48683
|
courseName: stringType().optional(),
|
|
48629
48684
|
studentEmail: stringType().email().optional()
|
|
48630
48685
|
});
|
|
@@ -48948,21 +49003,20 @@ function isCanonicalOrigin(value) {
|
|
|
48948
49003
|
}
|
|
48949
49004
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
48950
49005
|
}
|
|
48951
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema,
|
|
49006
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
48952
49007
|
var init_schemas5 = __esm(() => {
|
|
48953
49008
|
init_esm();
|
|
48954
49009
|
LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
|
|
48955
49010
|
EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
|
|
48956
49011
|
message: "Origin must be a bare https origin like https://example.com, with no path or slash"
|
|
48957
49012
|
});
|
|
48958
|
-
|
|
48959
|
-
|
|
48960
|
-
|
|
48961
|
-
|
|
48962
|
-
|
|
48963
|
-
|
|
49013
|
+
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
49014
|
+
EnsureEmbedPartnerSchema = objectType({
|
|
49015
|
+
name: stringType().trim().min(1).max(100)
|
|
49016
|
+
}).strict();
|
|
49017
|
+
AddEmbedPartnerOriginSchema = objectType({
|
|
49018
|
+
origin: EmbedOriginSchema
|
|
48964
49019
|
}).strict();
|
|
48965
|
-
UpdateEmbedPartnerSchema = CreateEmbedPartnerSchema.omit({ clientId: true }).partial();
|
|
48966
49020
|
CreateEmbedLaunchSchema = objectType({
|
|
48967
49021
|
timebackId: stringType().trim().min(1).max(200),
|
|
48968
49022
|
gameSlug: stringType().trim().min(1).max(100),
|
|
@@ -76120,6 +76174,7 @@ class CourseAssessments {
|
|
|
76120
76174
|
}
|
|
76121
76175
|
function buildCoursePayload(config) {
|
|
76122
76176
|
return {
|
|
76177
|
+
...config.course.sourcedId && { sourcedId: config.course.sourcedId },
|
|
76123
76178
|
status: "active",
|
|
76124
76179
|
title: config.course.title,
|
|
76125
76180
|
courseCode: config.course.courseCode,
|
|
@@ -76166,6 +76221,16 @@ function buildComponentResourcePayload(config, sourcedIds) {
|
|
|
76166
76221
|
lessonType: config.componentResource.lessonType
|
|
76167
76222
|
};
|
|
76168
76223
|
}
|
|
76224
|
+
async function findOrUndefined(request) {
|
|
76225
|
+
try {
|
|
76226
|
+
return await request;
|
|
76227
|
+
} catch (error) {
|
|
76228
|
+
if (isApiError(error) && error.statusCode === 404) {
|
|
76229
|
+
return;
|
|
76230
|
+
}
|
|
76231
|
+
throw error;
|
|
76232
|
+
}
|
|
76233
|
+
}
|
|
76169
76234
|
|
|
76170
76235
|
class CourseIntegration {
|
|
76171
76236
|
core;
|
|
@@ -76204,6 +76269,32 @@ class CourseIntegration {
|
|
|
76204
76269
|
...verboseData && { verboseData }
|
|
76205
76270
|
};
|
|
76206
76271
|
}
|
|
76272
|
+
async findCourse(courseId) {
|
|
76273
|
+
return findOrUndefined(this.core.api.oneroster.courses.get(courseId));
|
|
76274
|
+
}
|
|
76275
|
+
async resumeSetup(courseId, config) {
|
|
76276
|
+
const oneroster = this.core.api.oneroster;
|
|
76277
|
+
const sourcedIds = deriveSourcedIds(courseId);
|
|
76278
|
+
if (!await findOrUndefined(oneroster.courses.getComponent(sourcedIds.component))) {
|
|
76279
|
+
await oneroster.courses.createComponent(buildComponentPayload(config, sourcedIds));
|
|
76280
|
+
addEvent("timeback.component_created", {
|
|
76281
|
+
"app.timeback.component_id": sourcedIds.component
|
|
76282
|
+
});
|
|
76283
|
+
}
|
|
76284
|
+
if (!await findOrUndefined(oneroster.resources.get(sourcedIds.resource))) {
|
|
76285
|
+
await oneroster.resources.create(buildResourcePayload(config, sourcedIds.resource));
|
|
76286
|
+
addEvent("timeback.resource_created", {
|
|
76287
|
+
"app.timeback.resource_id": sourcedIds.resource
|
|
76288
|
+
});
|
|
76289
|
+
}
|
|
76290
|
+
if (!await findOrUndefined(oneroster.courses.getComponentResource(sourcedIds.componentResource))) {
|
|
76291
|
+
await oneroster.courses.createComponentResource(buildComponentResourcePayload(config, sourcedIds));
|
|
76292
|
+
addEvent("timeback.component_resource_link_created", {
|
|
76293
|
+
"app.timeback.component_resource_id": sourcedIds.componentResource
|
|
76294
|
+
});
|
|
76295
|
+
}
|
|
76296
|
+
await this.update(courseId, config);
|
|
76297
|
+
}
|
|
76207
76298
|
async update(courseId, config) {
|
|
76208
76299
|
const oneroster = this.core.api.oneroster;
|
|
76209
76300
|
const sourcedIds = deriveSourcedIds(courseId);
|
|
@@ -76343,6 +76434,8 @@ function createCourseNamespace(core) {
|
|
|
76343
76434
|
update: (courseId, config) => integration.update(courseId, config),
|
|
76344
76435
|
verify: (courseId) => integration.verify(courseId),
|
|
76345
76436
|
getConfig: (courseId) => integration.getConfig(courseId),
|
|
76437
|
+
findCourse: (courseId) => integration.findCourse(courseId),
|
|
76438
|
+
resumeSetup: (courseId, config) => integration.resumeSetup(courseId, config),
|
|
76346
76439
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
76347
76440
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
76348
76441
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
@@ -76427,7 +76520,9 @@ class RosterEnrollments {
|
|
|
76427
76520
|
status: "active",
|
|
76428
76521
|
grades,
|
|
76429
76522
|
subjects,
|
|
76430
|
-
school: enrollment.school
|
|
76523
|
+
school: enrollment.school,
|
|
76524
|
+
beginDate: enrollment.beginDate,
|
|
76525
|
+
...enrollment.metadata && { metadata: enrollment.metadata }
|
|
76431
76526
|
};
|
|
76432
76527
|
});
|
|
76433
76528
|
this.core.caches.enrollments.set(studentId, enrollments);
|
|
@@ -87231,7 +87326,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
87231
87326
|
}) {
|
|
87232
87327
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
87233
87328
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
87234
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
87329
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
87235
87330
|
});
|
|
87236
87331
|
const matchingIntegrations = new Map;
|
|
87237
87332
|
for (const integration of integrations) {
|
|
@@ -87620,7 +87715,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
87620
87715
|
}) {
|
|
87621
87716
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
87622
87717
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
87623
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
87718
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
87624
87719
|
});
|
|
87625
87720
|
const orderedIntegrations = integrations.toSorted((left, right) => {
|
|
87626
87721
|
const subjectDifference = left.subject.localeCompare(right.subject);
|
|
@@ -87699,7 +87794,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
87699
87794
|
async candidateIntegrations(gameId, studentId, input, db = this.deps.db) {
|
|
87700
87795
|
const filters = [
|
|
87701
87796
|
eq(gameTimebackIntegrations.gameId, gameId),
|
|
87702
|
-
|
|
87797
|
+
isActiveBaseGameTimebackIntegration()
|
|
87703
87798
|
];
|
|
87704
87799
|
if (input.subject) {
|
|
87705
87800
|
filters.push(eq(gameTimebackIntegrations.subject, input.subject));
|
|
@@ -90599,6 +90694,22 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
90599
90694
|
await init_errors8();
|
|
90600
90695
|
});
|
|
90601
90696
|
|
|
90697
|
+
// ../api-core/src/utils/timeback-attribution.util.ts
|
|
90698
|
+
function resolveRuntimeAttribution(base, holeFilling, requestedCourseId, recordedCourseId) {
|
|
90699
|
+
const recorded = [base, ...holeFilling].find((integration) => integration.courseId === recordedCourseId);
|
|
90700
|
+
if (recorded) {
|
|
90701
|
+
return { integration: recorded, requestedCourseIdStatus: "pinned_by_run" };
|
|
90702
|
+
}
|
|
90703
|
+
if (requestedCourseId === undefined) {
|
|
90704
|
+
return { integration: base, requestedCourseIdStatus: "absent" };
|
|
90705
|
+
}
|
|
90706
|
+
if (requestedCourseId === base.courseId) {
|
|
90707
|
+
return { integration: base, requestedCourseIdStatus: "accepted" };
|
|
90708
|
+
}
|
|
90709
|
+
const requestedHoleFilling = holeFilling.find((integration) => integration.courseId === requestedCourseId);
|
|
90710
|
+
return requestedHoleFilling ? { integration: requestedHoleFilling, requestedCourseIdStatus: "accepted_hole_filling" } : { integration: base, requestedCourseIdStatus: "ignored_mismatch" };
|
|
90711
|
+
}
|
|
90712
|
+
|
|
90602
90713
|
// ../api-core/src/utils/timeback-create-integration.util.ts
|
|
90603
90714
|
function buildTimebackBaseConfigFromExistingConfig(config) {
|
|
90604
90715
|
return {
|
|
@@ -90641,6 +90752,50 @@ function timebackConfigMatchesCreateIntegrationRequest(config, request) {
|
|
|
90641
90752
|
const requestedLevel = request.level ?? deriveTimebackCourseLevelFromGrade(request.grade);
|
|
90642
90753
|
return subject === request.subject && grade === request.grade && config.course.title === request.title && config.course.courseCode === request.courseCode && config.course.level === requestedLevel && (getTotalXpFromTimebackConfig(config) ?? null) === request.totalXp && (getMasterableUnitsFromTimebackConfig(config) ?? null) === request.masterableUnits;
|
|
90643
90754
|
}
|
|
90755
|
+
function buildHoleFillingCourseConfig(baseConfig, request) {
|
|
90756
|
+
const baseCourseMetadata = isRecord4(baseConfig.course.metadata) ? baseConfig.course.metadata : {};
|
|
90757
|
+
const baseMetrics = isRecord4(baseCourseMetadata.metrics) ? baseCourseMetadata.metrics : {};
|
|
90758
|
+
const baseResourceMetadata = isRecord4(baseConfig.resource.metadata) ? baseConfig.resource.metadata : {};
|
|
90759
|
+
const basePlaycademy = isPlaycademyResourceMetadata(baseResourceMetadata.playcademy) ? baseResourceMetadata.playcademy : undefined;
|
|
90760
|
+
return {
|
|
90761
|
+
organization: baseConfig.organization,
|
|
90762
|
+
course: {
|
|
90763
|
+
...baseConfig.course,
|
|
90764
|
+
...request.courseId && { sourcedId: request.courseId },
|
|
90765
|
+
title: request.title,
|
|
90766
|
+
courseCode: request.courseCode,
|
|
90767
|
+
metadata: {
|
|
90768
|
+
...baseCourseMetadata,
|
|
90769
|
+
courseType: HOLE_FILLING_COURSE_TYPE,
|
|
90770
|
+
baseCourseSourcedId: request.baseCourseId,
|
|
90771
|
+
metrics: {
|
|
90772
|
+
...baseMetrics,
|
|
90773
|
+
totalXp: request.totalXp,
|
|
90774
|
+
totalLessons: request.masterableUnits,
|
|
90775
|
+
totalGrades: 1
|
|
90776
|
+
}
|
|
90777
|
+
}
|
|
90778
|
+
},
|
|
90779
|
+
component: { ...baseConfig.component, title: `${request.title} Activities` },
|
|
90780
|
+
resource: {
|
|
90781
|
+
...baseConfig.resource,
|
|
90782
|
+
title: `${request.title} Game`,
|
|
90783
|
+
metadata: {
|
|
90784
|
+
...baseResourceMetadata,
|
|
90785
|
+
xp: request.totalXp,
|
|
90786
|
+
playcademy: {
|
|
90787
|
+
...basePlaycademy,
|
|
90788
|
+
mastery: {
|
|
90789
|
+
...basePlaycademy?.mastery,
|
|
90790
|
+
masterableUnits: request.masterableUnits
|
|
90791
|
+
}
|
|
90792
|
+
}
|
|
90793
|
+
}
|
|
90794
|
+
},
|
|
90795
|
+
componentResource: { ...baseConfig.componentResource, title: `${request.title} Lesson` }
|
|
90796
|
+
};
|
|
90797
|
+
}
|
|
90798
|
+
var HOLE_FILLING_COURSE_TYPE = "hole-filling";
|
|
90644
90799
|
var init_timeback_create_integration_util = __esm(() => {
|
|
90645
90800
|
init_constants2();
|
|
90646
90801
|
init_types2();
|
|
@@ -90656,7 +90811,7 @@ async function promoteCompletedCourse({
|
|
|
90656
90811
|
enrollments: prefetchedEnrollments
|
|
90657
90812
|
}) {
|
|
90658
90813
|
const subjectIntegrations = await db.query.gameTimebackIntegrations.findMany({
|
|
90659
|
-
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject),
|
|
90814
|
+
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveBaseGameTimebackIntegration())
|
|
90660
90815
|
});
|
|
90661
90816
|
const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
|
|
90662
90817
|
if (!nextIntegration) {
|
|
@@ -90723,6 +90878,8 @@ function toGameTimebackIntegration(integration) {
|
|
|
90723
90878
|
courseId: integration.courseId,
|
|
90724
90879
|
grade: integration.grade,
|
|
90725
90880
|
subject: integration.subject,
|
|
90881
|
+
kind: integration.kind,
|
|
90882
|
+
baseIntegrationId: integration.baseIntegrationId,
|
|
90726
90883
|
totalXp: integration.totalXp ?? null,
|
|
90727
90884
|
status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
|
|
90728
90885
|
deactivatedAt: integration.deactivatedAt ?? null,
|
|
@@ -90760,6 +90917,9 @@ var init_timeback_removed_integration_util = __esm(() => {
|
|
|
90760
90917
|
|
|
90761
90918
|
// ../api-core/src/services/timeback.service.ts
|
|
90762
90919
|
import { createHash as createHash3 } from "node:crypto";
|
|
90920
|
+
function isUnregisteredHoleFillingOf(course, base) {
|
|
90921
|
+
return course.metadata?.courseType === HOLE_FILLING_COURSE_TYPE && course.metadata.baseCourseSourcedId === base.courseId;
|
|
90922
|
+
}
|
|
90763
90923
|
async function findGameTimebackIntegrationForUpdate(db, condition) {
|
|
90764
90924
|
const [integration] = await db.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
|
|
90765
90925
|
return integration;
|
|
@@ -90809,13 +90969,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
90809
90969
|
static clearInFlightHeartbeatWindow(key) {
|
|
90810
90970
|
this.inFlightHeartbeatWindows.delete(key);
|
|
90811
90971
|
}
|
|
90812
|
-
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
90972
|
+
static buildCompletionCaliperEventId(key, kind, baseCourseId, discriminator = "completion") {
|
|
90813
90973
|
const hash = createHash3("sha256").update([
|
|
90814
90974
|
"playcademy",
|
|
90815
90975
|
"timeback",
|
|
90816
90976
|
kind,
|
|
90817
90977
|
key.gameId,
|
|
90818
|
-
|
|
90978
|
+
baseCourseId,
|
|
90819
90979
|
key.studentId,
|
|
90820
90980
|
key.runId,
|
|
90821
90981
|
key.activityId,
|
|
@@ -90881,11 +91041,14 @@ var init_timeback_service = __esm(async () => {
|
|
|
90881
91041
|
} : null
|
|
90882
91042
|
};
|
|
90883
91043
|
}
|
|
90884
|
-
static async acquireCompletionReservation(db, key) {
|
|
90885
|
-
|
|
91044
|
+
static async acquireCompletionReservation(db, key, otherCourseIds = []) {
|
|
91045
|
+
function reserve() {
|
|
91046
|
+
return otherCourseIds.length > 0 ? TimebackService.reserveCompletionAcrossCourses(db, key, otherCourseIds) : TimebackService.reserveCompletion(db, key);
|
|
91047
|
+
}
|
|
91048
|
+
let reserveOutcome = await reserve();
|
|
90886
91049
|
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
90887
91050
|
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
90888
|
-
reserveOutcome = await
|
|
91051
|
+
reserveOutcome = await reserve();
|
|
90889
91052
|
}
|
|
90890
91053
|
if (reserveOutcome.status === "duplicate") {
|
|
90891
91054
|
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
@@ -90904,8 +91067,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
90904
91067
|
}
|
|
90905
91068
|
return { status: "reserved", token: reserveOutcome.token };
|
|
90906
91069
|
}
|
|
90907
|
-
static async
|
|
90908
|
-
|
|
91070
|
+
static async reserveCompletionAcrossCourses(db, key, otherCourseIds) {
|
|
91071
|
+
return db.transaction(async (tx) => {
|
|
91072
|
+
const database = tx;
|
|
91073
|
+
const runKey = `timeback-completion:${key.gameId.toLowerCase()}:${key.studentId}:${key.runId.toLowerCase()}:${key.activityId}`;
|
|
91074
|
+
await database.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${runKey}, 0))`);
|
|
91075
|
+
const onOtherCourse = await database.query.gameTimebackActivityCompletions.findFirst({
|
|
91076
|
+
columns: { id: true },
|
|
91077
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, key.gameId), eq(gameTimebackActivityCompletions.studentId, key.studentId), eq(gameTimebackActivityCompletions.runId, key.runId), eq(gameTimebackActivityCompletions.activityId, key.activityId), inArray(gameTimebackActivityCompletions.courseId, [...otherCourseIds]))
|
|
91078
|
+
});
|
|
91079
|
+
if (onOtherCourse) {
|
|
91080
|
+
setAttribute("app.timeback.end_activity_status", "completion_reserved_on_other_course");
|
|
91081
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
91082
|
+
}
|
|
91083
|
+
return this.reserveCompletion(database, key);
|
|
91084
|
+
});
|
|
91085
|
+
}
|
|
91086
|
+
static async runWithCompletionReservation(db, key, emit, awardFromResult, otherCourseIds = []) {
|
|
91087
|
+
const acquired = await this.acquireCompletionReservation(db, key, otherCourseIds);
|
|
90909
91088
|
if (acquired.status === "duplicate") {
|
|
90910
91089
|
return acquired;
|
|
90911
91090
|
}
|
|
@@ -90940,6 +91119,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
90940
91119
|
await db.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
90941
91120
|
}
|
|
90942
91121
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
91122
|
+
return TimebackService.addPlaycademyExtensions(extensions, { resumeId });
|
|
91123
|
+
}
|
|
91124
|
+
static addPlaycademyExtensions(extensions, fields) {
|
|
90943
91125
|
const base = extensions ?? {};
|
|
90944
91126
|
const existingPlaycademy = base.playcademy;
|
|
90945
91127
|
const playcademy = typeof existingPlaycademy === "object" && existingPlaycademy !== null && !Array.isArray(existingPlaycademy) ? existingPlaycademy : {};
|
|
@@ -90947,28 +91129,71 @@ var init_timeback_service = __esm(async () => {
|
|
|
90947
91129
|
...base,
|
|
90948
91130
|
playcademy: {
|
|
90949
91131
|
...playcademy,
|
|
90950
|
-
|
|
91132
|
+
...fields
|
|
90951
91133
|
}
|
|
90952
91134
|
};
|
|
90953
91135
|
}
|
|
90954
|
-
|
|
90955
|
-
|
|
90956
|
-
|
|
90957
|
-
|
|
90958
|
-
|
|
90959
|
-
|
|
91136
|
+
async resolveRuntimeCourse({
|
|
91137
|
+
gameId,
|
|
91138
|
+
studentId,
|
|
91139
|
+
runId,
|
|
91140
|
+
activityData,
|
|
91141
|
+
extensions
|
|
91142
|
+
}) {
|
|
91143
|
+
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91144
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
91145
|
+
});
|
|
91146
|
+
const base = integrations.find(isBaseGameTimebackIntegrationRow);
|
|
91147
|
+
if (!base) {
|
|
91148
|
+
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
90960
91149
|
}
|
|
91150
|
+
const holeFilling = integrations.filter((integration) => !isBaseGameTimebackIntegrationRow(integration));
|
|
91151
|
+
const recordedCourseId = holeFilling.length > 0 && runId ? await this.findRecordedRunCourse({
|
|
91152
|
+
gameId,
|
|
91153
|
+
studentId,
|
|
91154
|
+
runId,
|
|
91155
|
+
activityId: activityData.activityId,
|
|
91156
|
+
courseIds: integrations.map((i2) => i2.courseId)
|
|
91157
|
+
}) : undefined;
|
|
91158
|
+
const { integration, requestedCourseIdStatus } = resolveRuntimeAttribution(base, holeFilling, activityData.courseId, recordedCourseId);
|
|
91159
|
+
const hintUsed = activityData.courseId === integration.courseId;
|
|
91160
|
+
const enrollmentId = hintUsed ? activityData.enrollmentId : undefined;
|
|
91161
|
+
const isHoleFilling = !isBaseGameTimebackIntegrationRow(integration);
|
|
90961
91162
|
return {
|
|
90962
|
-
|
|
90963
|
-
|
|
91163
|
+
integration,
|
|
91164
|
+
courseId: integration.courseId,
|
|
91165
|
+
baseCourseId: base.courseId,
|
|
91166
|
+
otherCourseIds: holeFilling.length > 0 ? integrations.map((other) => other.courseId).filter((courseId) => courseId !== integration.courseId) : [],
|
|
91167
|
+
courseName: activityData.courseId === undefined || hintUsed ? activityData.courseName : undefined,
|
|
91168
|
+
extensions: isHoleFilling ? TimebackService.addPlaycademyExtensions(extensions, {
|
|
91169
|
+
courseKind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
91170
|
+
baseCourseId: base.courseId,
|
|
91171
|
+
...enrollmentId && { enrollmentId }
|
|
91172
|
+
}) : extensions,
|
|
90964
91173
|
attributes: {
|
|
90965
|
-
"app.timeback.course_id":
|
|
90966
|
-
"app.timeback.resolved_course_id":
|
|
90967
|
-
"app.timeback.requested_course_id":
|
|
90968
|
-
"app.timeback.requested_course_id_status": requestedCourseIdStatus
|
|
91174
|
+
"app.timeback.course_id": integration.courseId,
|
|
91175
|
+
"app.timeback.resolved_course_id": integration.courseId,
|
|
91176
|
+
"app.timeback.requested_course_id": activityData.courseId,
|
|
91177
|
+
"app.timeback.requested_course_id_status": requestedCourseIdStatus,
|
|
91178
|
+
"app.timeback.course_kind": integration.kind,
|
|
91179
|
+
"app.timeback.enrollment_id": enrollmentId
|
|
90969
91180
|
}
|
|
90970
91181
|
};
|
|
90971
91182
|
}
|
|
91183
|
+
async findRecordedRunCourse({
|
|
91184
|
+
gameId,
|
|
91185
|
+
studentId,
|
|
91186
|
+
runId,
|
|
91187
|
+
activityId,
|
|
91188
|
+
courseIds
|
|
91189
|
+
}) {
|
|
91190
|
+
const recorded = await this.deps.db.query.gameTimebackActivityCompletions.findFirst({
|
|
91191
|
+
columns: { courseId: true },
|
|
91192
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, gameId), eq(gameTimebackActivityCompletions.studentId, studentId), eq(gameTimebackActivityCompletions.runId, runId), eq(gameTimebackActivityCompletions.activityId, activityId), inArray(gameTimebackActivityCompletions.courseId, courseIds)),
|
|
91193
|
+
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
91194
|
+
});
|
|
91195
|
+
return recorded?.courseId;
|
|
91196
|
+
}
|
|
90972
91197
|
static recordRuntimeContext({
|
|
90973
91198
|
operation,
|
|
90974
91199
|
studentId,
|
|
@@ -91166,13 +91391,29 @@ var init_timeback_service = __esm(async () => {
|
|
|
91166
91391
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
91167
91392
|
where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
|
|
91168
91393
|
});
|
|
91169
|
-
return mapEnrollmentsToUserEnrollments(enrollments, integrations);
|
|
91394
|
+
return mapEnrollmentsToUserEnrollments(enrollments, integrations, await this.loadBaseCourseIds(integrations));
|
|
91170
91395
|
} catch (error) {
|
|
91171
91396
|
setAttribute("app.timeback.enrollment_fetch", "failed");
|
|
91172
91397
|
setAttribute("app.timeback.enrollment_fetch_error", errorMessage(error));
|
|
91173
91398
|
return [];
|
|
91174
91399
|
}
|
|
91175
91400
|
}
|
|
91401
|
+
async loadBaseCourseIds(integrations) {
|
|
91402
|
+
const courseIdById = new Map(integrations.map((row) => [row.id, row.courseId]));
|
|
91403
|
+
const missingBaseIds = [
|
|
91404
|
+
...new Set(integrations.map((row) => row.baseIntegrationId).filter((id) => id !== null && !courseIdById.has(id)))
|
|
91405
|
+
];
|
|
91406
|
+
if (missingBaseIds.length > 0) {
|
|
91407
|
+
const baseRows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91408
|
+
columns: { id: true, courseId: true },
|
|
91409
|
+
where: inArray(gameTimebackIntegrations.id, missingBaseIds)
|
|
91410
|
+
});
|
|
91411
|
+
for (const row of baseRows) {
|
|
91412
|
+
courseIdById.set(row.id, row.courseId);
|
|
91413
|
+
}
|
|
91414
|
+
}
|
|
91415
|
+
return courseIdById;
|
|
91416
|
+
}
|
|
91176
91417
|
async setupIntegration(gameId, request, user) {
|
|
91177
91418
|
return this.withClientTelemetry(async () => {
|
|
91178
91419
|
const client = this.requireClient();
|
|
@@ -91183,7 +91424,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91183
91424
|
"app.timeback.course_count": courses.length
|
|
91184
91425
|
});
|
|
91185
91426
|
const existing = await db.query.gameTimebackIntegrations.findMany({
|
|
91186
|
-
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
91427
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration())
|
|
91187
91428
|
});
|
|
91188
91429
|
const integrations = [];
|
|
91189
91430
|
const verboseData = [];
|
|
@@ -91331,13 +91572,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
91331
91572
|
"app.timeback.subject": request.subject
|
|
91332
91573
|
});
|
|
91333
91574
|
const activeConflict = await db.query.gameTimebackIntegrations.findFirst({
|
|
91334
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject),
|
|
91575
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveBaseGameTimebackIntegration())
|
|
91335
91576
|
});
|
|
91336
91577
|
if (activeConflict) {
|
|
91337
91578
|
throw new ConflictError(`A TimeBack integration already exists for ${request.subject} Grade ${request.grade}`);
|
|
91338
91579
|
}
|
|
91339
91580
|
const removedCandidates = await db.query.gameTimebackIntegrations.findMany({
|
|
91340
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))
|
|
91581
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration())
|
|
91341
91582
|
});
|
|
91342
91583
|
for (const candidate of removedCandidates) {
|
|
91343
91584
|
const config = await client.course.getConfig(candidate.courseId).catch((error) => {
|
|
@@ -91375,10 +91616,10 @@ var init_timeback_service = __esm(async () => {
|
|
|
91375
91616
|
}
|
|
91376
91617
|
}
|
|
91377
91618
|
const reference = await db.query.gameTimebackIntegrations.findFirst({
|
|
91378
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91619
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration()),
|
|
91379
91620
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
91380
91621
|
}) ?? await db.query.gameTimebackIntegrations.findFirst({
|
|
91381
|
-
where: eq(gameTimebackIntegrations.gameId, gameId),
|
|
91622
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration()),
|
|
91382
91623
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
91383
91624
|
});
|
|
91384
91625
|
if (!reference) {
|
|
@@ -91408,10 +91649,77 @@ var init_timeback_service = __esm(async () => {
|
|
|
91408
91649
|
return integration;
|
|
91409
91650
|
});
|
|
91410
91651
|
}
|
|
91652
|
+
async createHoleFillingCourse(gameId, user, request) {
|
|
91653
|
+
return this.withClientTelemetry(async () => {
|
|
91654
|
+
const client = this.requireClient();
|
|
91655
|
+
const db = this.deps.db;
|
|
91656
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91657
|
+
TimebackService.recordIntegrationOperation("create_hole_filling_course", {
|
|
91658
|
+
"app.timeback.base_course_id": request.baseCourseId,
|
|
91659
|
+
"app.timeback.custom_course_id": Boolean(request.courseId)
|
|
91660
|
+
});
|
|
91661
|
+
const integration = await db.transaction(async (tx) => {
|
|
91662
|
+
const database = tx;
|
|
91663
|
+
const base = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, request.baseCourseId), isActiveBaseGameTimebackIntegration()));
|
|
91664
|
+
if (!base) {
|
|
91665
|
+
throw new NotFoundError("Active base course", `${gameId}:${request.baseCourseId}`);
|
|
91666
|
+
}
|
|
91667
|
+
const config = buildHoleFillingCourseConfig(await client.course.getConfig(base.courseId), request);
|
|
91668
|
+
let courseId = request.courseId;
|
|
91669
|
+
if (courseId) {
|
|
91670
|
+
const takenLocally = await database.query.gameTimebackIntegrations.findFirst({
|
|
91671
|
+
columns: { id: true },
|
|
91672
|
+
where: eq(gameTimebackIntegrations.courseId, courseId)
|
|
91673
|
+
});
|
|
91674
|
+
const remote = takenLocally ? undefined : await client.course.findCourse(courseId);
|
|
91675
|
+
if (takenLocally || remote && !isUnregisteredHoleFillingOf(remote, base)) {
|
|
91676
|
+
throw new ConflictError(`A Timeback course with ID "${courseId}" already exists`);
|
|
91677
|
+
}
|
|
91678
|
+
if (remote) {
|
|
91679
|
+
setAttribute("app.timeback.course_create_mode", "resumed");
|
|
91680
|
+
await client.course.resumeSetup(courseId, config);
|
|
91681
|
+
} else {
|
|
91682
|
+
courseId = undefined;
|
|
91683
|
+
}
|
|
91684
|
+
}
|
|
91685
|
+
if (!courseId) {
|
|
91686
|
+
const created = await client.course.setup(config);
|
|
91687
|
+
courseId = created.courseId;
|
|
91688
|
+
}
|
|
91689
|
+
setAttribute("app.timeback.course_id", courseId);
|
|
91690
|
+
const [inserted] = await database.insert(gameTimebackIntegrations).values({
|
|
91691
|
+
gameId,
|
|
91692
|
+
courseId,
|
|
91693
|
+
grade: base.grade,
|
|
91694
|
+
subject: base.subject,
|
|
91695
|
+
totalXp: request.totalXp,
|
|
91696
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
91697
|
+
baseIntegrationId: base.id
|
|
91698
|
+
}).returning();
|
|
91699
|
+
return inserted;
|
|
91700
|
+
});
|
|
91701
|
+
if (!integration) {
|
|
91702
|
+
throw new InternalError("Hole-filling course creation did not return an integration");
|
|
91703
|
+
}
|
|
91704
|
+
return toGameTimebackIntegration(integration);
|
|
91705
|
+
});
|
|
91706
|
+
}
|
|
91707
|
+
async getHoleFillingCourses(gameId, user) {
|
|
91708
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
91709
|
+
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91710
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.kind, HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND), isActiveGameTimebackIntegrationStatus()),
|
|
91711
|
+
orderBy: (table, { asc }) => [
|
|
91712
|
+
asc(table.subject),
|
|
91713
|
+
asc(table.grade),
|
|
91714
|
+
asc(table.createdAt)
|
|
91715
|
+
]
|
|
91716
|
+
});
|
|
91717
|
+
return rows.map((row) => toGameTimebackIntegration(row));
|
|
91718
|
+
}
|
|
91411
91719
|
async getIntegrations(gameId, user) {
|
|
91412
91720
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
91413
91721
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91414
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91722
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
91415
91723
|
});
|
|
91416
91724
|
return rows.map((row) => toGameTimebackIntegration(row));
|
|
91417
91725
|
}
|
|
@@ -91420,7 +91728,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91420
91728
|
const client = this.requireClient();
|
|
91421
91729
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
91422
91730
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91423
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
|
|
91731
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration()),
|
|
91424
91732
|
orderBy: (table, { asc }) => [asc(table.subject), asc(table.grade)]
|
|
91425
91733
|
});
|
|
91426
91734
|
setAttribute("app.timeback.removed_integration_count", rows.length);
|
|
@@ -91451,6 +91759,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
91451
91759
|
if (!integration) {
|
|
91452
91760
|
throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
|
|
91453
91761
|
}
|
|
91762
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
91763
|
+
throw new ConflictError("This is a hole-filling course and may be referenced by Timeback, so it can't be removed.");
|
|
91764
|
+
}
|
|
91765
|
+
const activeHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
91766
|
+
columns: { id: true },
|
|
91767
|
+
where: and(eq(gameTimebackIntegrations.baseIntegrationId, integration.id), isActiveGameTimebackIntegrationStatus())
|
|
91768
|
+
});
|
|
91769
|
+
if (activeHoleFilling) {
|
|
91770
|
+
setAttribute("app.timeback.removal_blocked", "hole_filling_courses");
|
|
91771
|
+
throw new ConflictError("This course has hole-filling courses that may be referenced by Timeback, so it can't be removed.");
|
|
91772
|
+
}
|
|
91454
91773
|
const activeEnrollments = await client.roster.getCourseRoster(courseId, {
|
|
91455
91774
|
role: "student",
|
|
91456
91775
|
includeUsers: false
|
|
@@ -91488,11 +91807,18 @@ var init_timeback_service = __esm(async () => {
|
|
|
91488
91807
|
if (!integration) {
|
|
91489
91808
|
throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
|
|
91490
91809
|
}
|
|
91491
|
-
|
|
91492
|
-
|
|
91493
|
-
|
|
91494
|
-
|
|
91495
|
-
|
|
91810
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
91811
|
+
const base = integration.baseIntegrationId ? await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.id, integration.baseIntegrationId), isActiveBaseGameTimebackIntegration())) : undefined;
|
|
91812
|
+
if (!base) {
|
|
91813
|
+
throw new ConflictError("Restore the base course first: a hole-filling course can only be active beside its base course.");
|
|
91814
|
+
}
|
|
91815
|
+
} else {
|
|
91816
|
+
const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
91817
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveBaseGameTimebackIntegration())
|
|
91818
|
+
});
|
|
91819
|
+
if (activeConflict) {
|
|
91820
|
+
throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
|
|
91821
|
+
}
|
|
91496
91822
|
}
|
|
91497
91823
|
const now = new Date;
|
|
91498
91824
|
const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
|
|
@@ -91531,52 +91857,66 @@ var init_timeback_service = __esm(async () => {
|
|
|
91531
91857
|
TimebackService.recordIntegrationOperation("update_integration", {
|
|
91532
91858
|
"app.timeback.course_id": courseId
|
|
91533
91859
|
});
|
|
91534
|
-
const
|
|
91535
|
-
|
|
91536
|
-
|
|
91537
|
-
|
|
91538
|
-
|
|
91539
|
-
|
|
91540
|
-
|
|
91541
|
-
|
|
91542
|
-
|
|
91543
|
-
|
|
91544
|
-
|
|
91545
|
-
|
|
91546
|
-
|
|
91547
|
-
|
|
91548
|
-
|
|
91549
|
-
|
|
91550
|
-
});
|
|
91551
|
-
if (subject !== integration.subject) {
|
|
91552
|
-
const subjectConflict = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
91553
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus())
|
|
91860
|
+
const updated = await this.deps.db.transaction(async (tx) => {
|
|
91861
|
+
const database = tx;
|
|
91862
|
+
const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
|
|
91863
|
+
if (!integration) {
|
|
91864
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
91865
|
+
}
|
|
91866
|
+
const timebackConfig = await client.course.getConfig(courseId);
|
|
91867
|
+
const liveSubject = timebackConfig.course.subjects[0];
|
|
91868
|
+
const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
|
|
91869
|
+
if (!isTimebackSubject(subject)) {
|
|
91870
|
+
throw new ValidationError(`Invalid subject "${subject}"`);
|
|
91871
|
+
}
|
|
91872
|
+
setAttributes({
|
|
91873
|
+
"app.timeback.grade": integration.grade,
|
|
91874
|
+
"app.timeback.previous_subject": integration.subject,
|
|
91875
|
+
"app.timeback.next_subject": subject
|
|
91554
91876
|
});
|
|
91555
|
-
if (
|
|
91556
|
-
|
|
91877
|
+
if (subject !== integration.subject) {
|
|
91878
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
91879
|
+
throw new ValidationError("A hole-filling course must keep the subject of its base course");
|
|
91880
|
+
}
|
|
91881
|
+
const linkedHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
91882
|
+
columns: { id: true },
|
|
91883
|
+
where: eq(gameTimebackIntegrations.baseIntegrationId, integration.id)
|
|
91884
|
+
});
|
|
91885
|
+
if (linkedHoleFilling) {
|
|
91886
|
+
throw new ValidationError("This course has hole-filling courses, so its subject cannot change");
|
|
91887
|
+
}
|
|
91557
91888
|
}
|
|
91558
|
-
|
|
91559
|
-
|
|
91560
|
-
|
|
91561
|
-
|
|
91562
|
-
|
|
91563
|
-
|
|
91889
|
+
if (subject !== integration.subject) {
|
|
91890
|
+
const subjectConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
91891
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveBaseGameTimebackIntegration())
|
|
91892
|
+
});
|
|
91893
|
+
if (subjectConflict && subjectConflict.id !== integration.id) {
|
|
91894
|
+
throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
|
|
91895
|
+
}
|
|
91896
|
+
}
|
|
91897
|
+
const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
|
|
91898
|
+
const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
|
|
91899
|
+
setAttributes({
|
|
91900
|
+
"app.timeback.total_xp": totalXp,
|
|
91901
|
+
"app.timeback.masterable_units": masterableUnits
|
|
91902
|
+
});
|
|
91903
|
+
await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
|
|
91904
|
+
...patch,
|
|
91905
|
+
subject,
|
|
91906
|
+
totalXp,
|
|
91907
|
+
masterableUnits,
|
|
91908
|
+
grade: integration.grade
|
|
91909
|
+
}));
|
|
91910
|
+
const [row] = await database.update(gameTimebackIntegrations).set({
|
|
91911
|
+
subject,
|
|
91912
|
+
totalXp,
|
|
91913
|
+
updatedAt: new Date
|
|
91914
|
+
}).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
|
|
91915
|
+
if (!row) {
|
|
91916
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
91917
|
+
}
|
|
91918
|
+
return row;
|
|
91564
91919
|
});
|
|
91565
|
-
await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
|
|
91566
|
-
...patch,
|
|
91567
|
-
subject,
|
|
91568
|
-
totalXp,
|
|
91569
|
-
masterableUnits,
|
|
91570
|
-
grade: integration.grade
|
|
91571
|
-
}));
|
|
91572
|
-
const [updated] = await this.deps.db.update(gameTimebackIntegrations).set({
|
|
91573
|
-
subject,
|
|
91574
|
-
totalXp,
|
|
91575
|
-
updatedAt: new Date
|
|
91576
|
-
}).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
|
|
91577
|
-
if (!updated) {
|
|
91578
|
-
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
91579
|
-
}
|
|
91580
91920
|
return toGameTimebackIntegration(updated);
|
|
91581
91921
|
});
|
|
91582
91922
|
}
|
|
@@ -91587,7 +91927,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91587
91927
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91588
91928
|
TimebackService.recordIntegrationOperation("verify_integration");
|
|
91589
91929
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
91590
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91930
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
91591
91931
|
});
|
|
91592
91932
|
if (integrations.length === 0) {
|
|
91593
91933
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -91625,7 +91965,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91625
91965
|
const client = this.requireClient();
|
|
91626
91966
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91627
91967
|
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
91628
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91968
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
91629
91969
|
});
|
|
91630
91970
|
if (!integration) {
|
|
91631
91971
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -91639,17 +91979,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
91639
91979
|
const db = this.deps.db;
|
|
91640
91980
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91641
91981
|
TimebackService.recordIntegrationOperation("delete_integrations");
|
|
91642
|
-
|
|
91643
|
-
|
|
91982
|
+
await db.transaction(async (tx) => {
|
|
91983
|
+
const database = tx;
|
|
91984
|
+
await database.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId)).for("update");
|
|
91985
|
+
const integrations = await database.select().from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
91986
|
+
if (integrations.length === 0) {
|
|
91987
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
91988
|
+
}
|
|
91989
|
+
setAttribute("app.timeback.integration_count", integrations.length);
|
|
91990
|
+
const holeFillingCount = integrations.filter((integration) => integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND).length;
|
|
91991
|
+
if (holeFillingCount > 0) {
|
|
91992
|
+
setAttribute("app.timeback.hole_filling_count", holeFillingCount);
|
|
91993
|
+
throw new ConflictError(`This game has ${holeFillingCount} hole-filling course(s) that may be referenced by Timeback, so its Timeback courses can't be removed automatically. Contact a Playcademy admin.`);
|
|
91994
|
+
}
|
|
91995
|
+
for (const integration of integrations) {
|
|
91996
|
+
await client.course.cleanup(integration.courseId);
|
|
91997
|
+
}
|
|
91998
|
+
await database.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
91644
91999
|
});
|
|
91645
|
-
if (integrations.length === 0) {
|
|
91646
|
-
throw new NotFoundError("Timeback integration", gameId);
|
|
91647
|
-
}
|
|
91648
|
-
setAttribute("app.timeback.integration_count", integrations.length);
|
|
91649
|
-
for (const integration of integrations) {
|
|
91650
|
-
await client.course.cleanup(integration.courseId);
|
|
91651
|
-
}
|
|
91652
|
-
await db.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
91653
92000
|
});
|
|
91654
92001
|
}
|
|
91655
92002
|
static patchCourseMetadata(metadata2, totalXp, options) {
|
|
@@ -91828,17 +92175,21 @@ var init_timeback_service = __esm(async () => {
|
|
|
91828
92175
|
activityData
|
|
91829
92176
|
});
|
|
91830
92177
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91831
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
91832
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
91833
|
-
});
|
|
91834
|
-
if (!integration) {
|
|
91835
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
91836
|
-
}
|
|
91837
92178
|
const {
|
|
92179
|
+
integration,
|
|
91838
92180
|
courseId: runtimeCourseId,
|
|
92181
|
+
baseCourseId,
|
|
92182
|
+
otherCourseIds,
|
|
91839
92183
|
courseName: runtimeCourseName,
|
|
92184
|
+
extensions: runtimeExtensions,
|
|
91840
92185
|
attributes: courseAttributes
|
|
91841
|
-
} =
|
|
92186
|
+
} = await this.resolveRuntimeCourse({
|
|
92187
|
+
gameId,
|
|
92188
|
+
studentId,
|
|
92189
|
+
runId,
|
|
92190
|
+
activityData,
|
|
92191
|
+
extensions: extensionsWithResumeId
|
|
92192
|
+
});
|
|
91842
92193
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
91843
92194
|
const dedupeKey = runId !== undefined ? {
|
|
91844
92195
|
gameId,
|
|
@@ -91867,12 +92218,12 @@ var init_timeback_service = __esm(async () => {
|
|
|
91867
92218
|
courseId: runtimeCourseId,
|
|
91868
92219
|
courseName: runtimeCourseName,
|
|
91869
92220
|
studentEmail: activityData.studentEmail,
|
|
91870
|
-
extensions:
|
|
92221
|
+
extensions: runtimeExtensions,
|
|
91871
92222
|
...runId ? { runId } : {}
|
|
91872
92223
|
};
|
|
91873
|
-
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
91874
|
-
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
91875
|
-
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
92224
|
+
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion", baseCourseId) : undefined;
|
|
92225
|
+
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", baseCourseId, effectiveResumeId) : undefined;
|
|
92226
|
+
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed", baseCourseId) : undefined;
|
|
91876
92227
|
function emitRecord() {
|
|
91877
92228
|
return client.activity.record(runtimeCourseId, studentId, {
|
|
91878
92229
|
...activityEventContext,
|
|
@@ -91896,7 +92247,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91896
92247
|
xpAwarded: emitted.xpAwarded,
|
|
91897
92248
|
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
91898
92249
|
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
91899
|
-
}));
|
|
92250
|
+
}), otherCourseIds);
|
|
91900
92251
|
if (outcome.status === "duplicate") {
|
|
91901
92252
|
blockedByDedupeGuard = true;
|
|
91902
92253
|
duplicateAward = outcome.award;
|
|
@@ -91944,6 +92295,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91944
92295
|
return {
|
|
91945
92296
|
status: "ok",
|
|
91946
92297
|
courseId: runtimeCourseId,
|
|
92298
|
+
courseKind: integration.kind,
|
|
91947
92299
|
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
91948
92300
|
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
91949
92301
|
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
@@ -91956,6 +92308,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91956
92308
|
return {
|
|
91957
92309
|
status: "ok",
|
|
91958
92310
|
courseId: runtimeCourseId,
|
|
92311
|
+
courseKind: integration.kind,
|
|
91959
92312
|
xpAwarded: result.xpAwarded,
|
|
91960
92313
|
masteredUnits: result.masteredUnitsApplied,
|
|
91961
92314
|
pctCompleteApp: result.pctCompleteApp,
|
|
@@ -91972,7 +92325,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91972
92325
|
const client = this.requireClient();
|
|
91973
92326
|
const db = this.deps.db;
|
|
91974
92327
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
91975
|
-
where:
|
|
92328
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), subject ? eq(gameTimebackIntegrations.subject, subject) : undefined, isActiveBaseGameTimebackIntegration())
|
|
91976
92329
|
});
|
|
91977
92330
|
if (integrations.length === 0) {
|
|
91978
92331
|
throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
|
|
@@ -92132,7 +92485,6 @@ var init_timeback_service = __esm(async () => {
|
|
|
92132
92485
|
}) {
|
|
92133
92486
|
return this.withClientTelemetry(async () => {
|
|
92134
92487
|
const client = this.requireClient();
|
|
92135
|
-
const db = this.deps.db;
|
|
92136
92488
|
const hasWindowStartedAtMs = windowStartedAtMs !== undefined;
|
|
92137
92489
|
const hasWindowSequence = windowSequence !== undefined;
|
|
92138
92490
|
if (hasWindowStartedAtMs === hasWindowSequence) {
|
|
@@ -92160,17 +92512,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
92160
92512
|
return inFlightHeartbeat;
|
|
92161
92513
|
}
|
|
92162
92514
|
const pendingHeartbeat = (async () => {
|
|
92163
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
92164
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
92165
|
-
});
|
|
92166
|
-
if (!integration) {
|
|
92167
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
92168
|
-
}
|
|
92169
92515
|
const {
|
|
92170
92516
|
courseId: runtimeCourseId,
|
|
92171
92517
|
courseName: runtimeCourseName,
|
|
92518
|
+
extensions: runtimeExtensions,
|
|
92172
92519
|
attributes: courseAttributes
|
|
92173
|
-
} =
|
|
92520
|
+
} = await this.resolveRuntimeCourse({
|
|
92521
|
+
gameId,
|
|
92522
|
+
studentId,
|
|
92523
|
+
activityData,
|
|
92524
|
+
extensions: TimebackService.addResumeIdToExtensions(undefined, effectiveResumeId)
|
|
92525
|
+
});
|
|
92174
92526
|
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
92175
92527
|
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
92176
92528
|
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
@@ -92186,7 +92538,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92186
92538
|
courseId: runtimeCourseId,
|
|
92187
92539
|
courseName: runtimeCourseName,
|
|
92188
92540
|
studentEmail: activityData.studentEmail,
|
|
92189
|
-
extensions:
|
|
92541
|
+
extensions: runtimeExtensions,
|
|
92190
92542
|
...runId ? { runId } : {}
|
|
92191
92543
|
});
|
|
92192
92544
|
}
|
|
@@ -92291,7 +92643,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92291
92643
|
const db = this.deps.db;
|
|
92292
92644
|
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
92293
92645
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
92294
|
-
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject),
|
|
92646
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveBaseGameTimebackIntegration())
|
|
92295
92647
|
});
|
|
92296
92648
|
if (!integration) {
|
|
92297
92649
|
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
@@ -93160,9 +93512,47 @@ var init_account_service = __esm(async () => {
|
|
|
93160
93512
|
await init_errors8();
|
|
93161
93513
|
});
|
|
93162
93514
|
|
|
93515
|
+
// ../api-core/src/services/embed-partner.service.ts
|
|
93516
|
+
class EmbedPartnerService {
|
|
93517
|
+
deps;
|
|
93518
|
+
constructor(deps) {
|
|
93519
|
+
this.deps = deps;
|
|
93520
|
+
}
|
|
93521
|
+
async ensurePartner(clientId, name2) {
|
|
93522
|
+
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
|
|
93523
|
+
target: embedPartners.clientId,
|
|
93524
|
+
set: { name: name2, updatedAt: new Date }
|
|
93525
|
+
}).returning();
|
|
93526
|
+
if (!partner) {
|
|
93527
|
+
throw new InternalError("Failed to ensure embed partner");
|
|
93528
|
+
}
|
|
93529
|
+
return partner;
|
|
93530
|
+
}
|
|
93531
|
+
addOrigin(clientId, origin) {
|
|
93532
|
+
return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
|
|
93533
|
+
THEN ${embedPartners.allowedOrigins}
|
|
93534
|
+
ELSE array_append(${embedPartners.allowedOrigins}, ${origin}) END`);
|
|
93535
|
+
}
|
|
93536
|
+
removeOrigin(clientId, origin) {
|
|
93537
|
+
return this.setOrigins(clientId, sql`array_remove(${embedPartners.allowedOrigins}, ${origin})`);
|
|
93538
|
+
}
|
|
93539
|
+
async setOrigins(clientId, origins) {
|
|
93540
|
+
const [partner] = await this.deps.db.update(embedPartners).set({ allowedOrigins: origins, updatedAt: new Date }).where(eq(embedPartners.clientId, clientId)).returning();
|
|
93541
|
+
if (!partner) {
|
|
93542
|
+
throw new NotFoundError("Embed partner", clientId);
|
|
93543
|
+
}
|
|
93544
|
+
return partner;
|
|
93545
|
+
}
|
|
93546
|
+
}
|
|
93547
|
+
var init_embed_partner_service = __esm(() => {
|
|
93548
|
+
init_drizzle_orm();
|
|
93549
|
+
init_tables_index();
|
|
93550
|
+
init_errors2();
|
|
93551
|
+
});
|
|
93552
|
+
|
|
93163
93553
|
// ../api-core/src/utils/embed.util.ts
|
|
93164
|
-
function canPartnerLaunch(partner, origin
|
|
93165
|
-
return partner.enabled && partner.allowedOrigins.includes(origin)
|
|
93554
|
+
function canPartnerLaunch(partner, origin) {
|
|
93555
|
+
return partner.enabled && partner.allowedOrigins.includes(origin);
|
|
93166
93556
|
}
|
|
93167
93557
|
function embedPageUrl(hubOrigin, slug, parentOrigin) {
|
|
93168
93558
|
const url = new URL(`${EMBED_PAGE_PATH}/${encodeURIComponent(slug)}`, hubOrigin);
|
|
@@ -93190,12 +93580,9 @@ class EmbedService {
|
|
|
93190
93580
|
return partner;
|
|
93191
93581
|
}
|
|
93192
93582
|
async isFrameOriginAllowed(origin, slug) {
|
|
93193
|
-
const
|
|
93194
|
-
|
|
93195
|
-
|
|
93196
|
-
gameId: embedPartnerGames.gameId
|
|
93197
|
-
}).from(embedPartnerGames).innerJoin(embedPartners, eq(embedPartners.id, embedPartnerGames.partnerId)).innerJoin(games, eq(games.id, embedPartnerGames.gameId)).where(eq(games.slug, slug));
|
|
93198
|
-
return grants.some((grant) => canPartnerLaunch({ ...grant, games: [grant] }, origin, grant.gameId));
|
|
93583
|
+
const game = this.deps.db.select({ id: games.id }).from(games).where(eq(games.slug, slug));
|
|
93584
|
+
const [partner] = await this.deps.db.select({ id: embedPartners.id }).from(embedPartners).where(and(eq(embedPartners.enabled, true), arrayContains(embedPartners.allowedOrigins, [origin]), exists(game))).limit(1);
|
|
93585
|
+
return partner !== undefined;
|
|
93199
93586
|
}
|
|
93200
93587
|
async createLaunch(partner, input) {
|
|
93201
93588
|
const game = await this.deps.db.query.games.findFirst({
|
|
@@ -93206,9 +93593,8 @@ class EmbedService {
|
|
|
93206
93593
|
throw new NotFoundError("Game", input.gameSlug);
|
|
93207
93594
|
}
|
|
93208
93595
|
setAttributes({ "app.game.id": game.id, "app.game.slug": game.slug });
|
|
93209
|
-
|
|
93210
|
-
|
|
93211
|
-
throw new AccessDeniedError("This partner may not launch that game into that origin");
|
|
93596
|
+
if (!canPartnerLaunch(partner, input.parentOrigin)) {
|
|
93597
|
+
throw new AccessDeniedError("This partner may not launch into that origin");
|
|
93212
93598
|
}
|
|
93213
93599
|
setAttributes({ "app.user.timeback_id": input.timebackId });
|
|
93214
93600
|
const user = await this.deps.resolveTimebackStudent(input.timebackId);
|
|
@@ -93270,20 +93656,13 @@ class EmbedService {
|
|
|
93270
93656
|
throw new NotFoundError("Embed launch", launchId);
|
|
93271
93657
|
}
|
|
93272
93658
|
setAttributes({ "app.game.id": launch.gameId, "app.user.id": launch.userId });
|
|
93273
|
-
const
|
|
93274
|
-
const outcome = renewalRefusal(launch, () => canPartnerLaunch({ ...partner, games: grants }, launch.parentOrigin, launch.gameId));
|
|
93659
|
+
const outcome = renewalRefusal(launch, () => canPartnerLaunch(partner, launch.parentOrigin));
|
|
93275
93660
|
setAttributes({ "app.embed.outcome": outcome ?? "renewed" });
|
|
93276
93661
|
if (outcome) {
|
|
93277
93662
|
throw new AccessDeniedError("This launch has ended");
|
|
93278
93663
|
}
|
|
93279
93664
|
return this.deps.mintGameToken(launch.gameId, launch.userId);
|
|
93280
93665
|
}
|
|
93281
|
-
grantsFor(partnerId, gameId) {
|
|
93282
|
-
return this.deps.db.query.embedPartnerGames.findMany({
|
|
93283
|
-
where: and(eq(embedPartnerGames.partnerId, partnerId), eq(embedPartnerGames.gameId, gameId)),
|
|
93284
|
-
columns: { gameId: true }
|
|
93285
|
-
});
|
|
93286
|
-
}
|
|
93287
93666
|
async endLaunch(launchId) {
|
|
93288
93667
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
93289
93668
|
}
|
|
@@ -93300,15 +93679,12 @@ class EmbedService {
|
|
|
93300
93679
|
"app.embed.partner_id": launch.partnerId,
|
|
93301
93680
|
"app.user.id": launch.userId
|
|
93302
93681
|
});
|
|
93303
|
-
const
|
|
93304
|
-
|
|
93305
|
-
|
|
93306
|
-
|
|
93307
|
-
}),
|
|
93308
|
-
this.grantsFor(launch.partnerId, launch.gameId)
|
|
93309
|
-
]);
|
|
93682
|
+
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
93683
|
+
where: eq(embedPartners.id, launch.partnerId),
|
|
93684
|
+
columns: { enabled: true, allowedOrigins: true }
|
|
93685
|
+
});
|
|
93310
93686
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
93311
|
-
const stillAllowed = partner !== undefined && canPartnerLaunch(
|
|
93687
|
+
const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
|
|
93312
93688
|
if (!originMatches || !stillAllowed) {
|
|
93313
93689
|
await this.endLaunch(launch.id);
|
|
93314
93690
|
setAttributes({
|
|
@@ -93726,14 +94102,16 @@ function createStandaloneServices(deps) {
|
|
|
93726
94102
|
return { ...profile, timeback: timebackData };
|
|
93727
94103
|
}
|
|
93728
94104
|
});
|
|
94105
|
+
const embedPartner = new EmbedPartnerService({ db });
|
|
93729
94106
|
const verify = new VerifyService({
|
|
93730
94107
|
db,
|
|
93731
94108
|
validateGameToken: auth.validateGameToken.bind(auth)
|
|
93732
94109
|
});
|
|
93733
|
-
return { account, embed, lti, session, user, verify };
|
|
94110
|
+
return { account, embed, embedPartner, lti, session, user, verify };
|
|
93734
94111
|
}
|
|
93735
94112
|
var init_standalone = __esm(async () => {
|
|
93736
94113
|
init_spans();
|
|
94114
|
+
init_embed_partner_service();
|
|
93737
94115
|
init_embed_service();
|
|
93738
94116
|
init_lti_service();
|
|
93739
94117
|
init_session_service();
|
|
@@ -110178,7 +110556,7 @@ function ilike2(column6, value) {
|
|
|
110178
110556
|
function notIlike2(column6, value) {
|
|
110179
110557
|
return sql4`${column6} not ilike ${value}`;
|
|
110180
110558
|
}
|
|
110181
|
-
function
|
|
110559
|
+
function arrayContains2(column6, values) {
|
|
110182
110560
|
if (Array.isArray(values)) {
|
|
110183
110561
|
if (values.length === 0) {
|
|
110184
110562
|
throw new Error("arrayContains requires at least one value");
|
|
@@ -133497,7 +133875,7 @@ params: ${params}`);
|
|
|
133497
133875
|
and: () => and2,
|
|
133498
133876
|
applyMixins: () => applyMixins2,
|
|
133499
133877
|
arrayContained: () => arrayContained,
|
|
133500
|
-
arrayContains: () =>
|
|
133878
|
+
arrayContains: () => arrayContains2,
|
|
133501
133879
|
arrayOverlaps: () => arrayOverlaps,
|
|
133502
133880
|
asc: () => asc2,
|
|
133503
133881
|
avg: () => avg,
|
|
@@ -150067,15 +150445,36 @@ function requireUuidParam(value, name2) {
|
|
|
150067
150445
|
}
|
|
150068
150446
|
return value;
|
|
150069
150447
|
}
|
|
150448
|
+
function bearerToken(request) {
|
|
150449
|
+
const header = request.headers.get("Authorization");
|
|
150450
|
+
return header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
150451
|
+
}
|
|
150070
150452
|
async function requirePartner(ctx) {
|
|
150071
|
-
const
|
|
150072
|
-
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
150453
|
+
const token = bearerToken(ctx.request);
|
|
150073
150454
|
const verified = token ? await ctx.providers.auth.verifyEmbedPartnerToken(token) : null;
|
|
150074
150455
|
if (!verified) {
|
|
150075
150456
|
throw ApiError.unauthorized("A valid embed partner access token is required");
|
|
150076
150457
|
}
|
|
150077
150458
|
return ctx.services.embed.getPartnerByClientId(verified.clientId);
|
|
150078
150459
|
}
|
|
150460
|
+
async function requireEmbedAdmin(ctx) {
|
|
150461
|
+
const expected = ctx.config.embedAdminToken;
|
|
150462
|
+
if (!expected) {
|
|
150463
|
+
throw ApiError.serviceUnavailable("Embed partner management is not configured");
|
|
150464
|
+
}
|
|
150465
|
+
const token = bearerToken(ctx.request);
|
|
150466
|
+
const [given, wanted] = token ? await Promise.all([sha256Hex(token), sha256Hex(expected)]) : [];
|
|
150467
|
+
if (!given || given !== wanted) {
|
|
150468
|
+
throw ApiError.unauthorized("A valid embed admin token is required");
|
|
150469
|
+
}
|
|
150470
|
+
}
|
|
150471
|
+
function requireClientIdParam(ctx) {
|
|
150472
|
+
const clientId = EmbedPartnerClientIdSchema.safeParse(ctx.params.clientId);
|
|
150473
|
+
if (!clientId.success) {
|
|
150474
|
+
throw ApiError.badRequest("Missing or invalid clientId");
|
|
150475
|
+
}
|
|
150476
|
+
return clientId.data;
|
|
150477
|
+
}
|
|
150079
150478
|
async function createLaunch(ctx) {
|
|
150080
150479
|
const partner = await requirePartner(ctx);
|
|
150081
150480
|
const body2 = await parseRequestBody(ctx.request, CreateEmbedLaunchSchema);
|
|
@@ -150105,6 +150504,27 @@ async function redeemPass(ctx) {
|
|
|
150105
150504
|
timeback: launch.profile.timeback
|
|
150106
150505
|
};
|
|
150107
150506
|
}
|
|
150507
|
+
async function ensurePartner(ctx) {
|
|
150508
|
+
await requireEmbedAdmin(ctx);
|
|
150509
|
+
const clientId = requireClientIdParam(ctx);
|
|
150510
|
+
const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
|
|
150511
|
+
return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
|
|
150512
|
+
}
|
|
150513
|
+
async function addPartnerOrigin(ctx) {
|
|
150514
|
+
await requireEmbedAdmin(ctx);
|
|
150515
|
+
const clientId = requireClientIdParam(ctx);
|
|
150516
|
+
const body2 = await parseRequestBody(ctx.request, AddEmbedPartnerOriginSchema);
|
|
150517
|
+
return ctx.services.embedPartner.addOrigin(clientId, body2.origin);
|
|
150518
|
+
}
|
|
150519
|
+
async function removePartnerOrigin(ctx) {
|
|
150520
|
+
await requireEmbedAdmin(ctx);
|
|
150521
|
+
const clientId = requireClientIdParam(ctx);
|
|
150522
|
+
const origin = EmbedOriginSchema.safeParse(ctx.url.searchParams.get("origin"));
|
|
150523
|
+
if (!origin.success) {
|
|
150524
|
+
throw ApiError.badRequest("Missing or invalid origin");
|
|
150525
|
+
}
|
|
150526
|
+
return ctx.services.embedPartner.removeOrigin(clientId, origin.data);
|
|
150527
|
+
}
|
|
150108
150528
|
var embed2;
|
|
150109
150529
|
var init_embed_controller = __esm(() => {
|
|
150110
150530
|
init_src();
|
|
@@ -150114,8 +150534,11 @@ var init_embed_controller = __esm(() => {
|
|
|
150114
150534
|
init_utils7();
|
|
150115
150535
|
init_embed_util();
|
|
150116
150536
|
embed2 = defineControllerNames("embed", {
|
|
150537
|
+
addPartnerOrigin,
|
|
150117
150538
|
createLaunch,
|
|
150539
|
+
ensurePartner,
|
|
150118
150540
|
redeemPass,
|
|
150541
|
+
removePartnerOrigin,
|
|
150119
150542
|
renewToken
|
|
150120
150543
|
});
|
|
150121
150544
|
});
|
|
@@ -150722,7 +151145,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
150722
151145
|
limit
|
|
150723
151146
|
};
|
|
150724
151147
|
}
|
|
150725
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
151148
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, getHoleFillingCourses, createHoleFillingCourse, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
150726
151149
|
var init_timeback_controller = __esm(() => {
|
|
150727
151150
|
init_esm();
|
|
150728
151151
|
init_src();
|
|
@@ -150801,6 +151224,27 @@ var init_timeback_controller = __esm(() => {
|
|
|
150801
151224
|
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
|
|
150802
151225
|
return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
|
|
150803
151226
|
});
|
|
151227
|
+
getHoleFillingCourses = requireGameManagementAccess(async (ctx) => {
|
|
151228
|
+
const gameId = ctx.params.gameId;
|
|
151229
|
+
if (!gameId) {
|
|
151230
|
+
throw ApiError.badRequest("Missing gameId");
|
|
151231
|
+
}
|
|
151232
|
+
if (!isValidUUID(gameId)) {
|
|
151233
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
151234
|
+
}
|
|
151235
|
+
return ctx.services.timeback.getHoleFillingCourses(gameId, ctx.user);
|
|
151236
|
+
});
|
|
151237
|
+
createHoleFillingCourse = requireDeveloper(async (ctx) => {
|
|
151238
|
+
const gameId = ctx.params.gameId;
|
|
151239
|
+
if (!gameId) {
|
|
151240
|
+
throw ApiError.badRequest("Missing gameId");
|
|
151241
|
+
}
|
|
151242
|
+
if (!isValidUUID(gameId)) {
|
|
151243
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
151244
|
+
}
|
|
151245
|
+
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackHoleFillingCourseRequestSchema);
|
|
151246
|
+
return ctx.services.timeback.createHoleFillingCourse(gameId, ctx.user, body2);
|
|
151247
|
+
});
|
|
150804
151248
|
updateIntegration = requireDeveloper(async (ctx) => {
|
|
150805
151249
|
const { gameId, courseId } = ctx.params;
|
|
150806
151250
|
if (!gameId || !courseId) {
|
|
@@ -151585,6 +152029,8 @@ var init_timeback_controller = __esm(() => {
|
|
|
151585
152029
|
getIntegrations,
|
|
151586
152030
|
getRemovedIntegrations,
|
|
151587
152031
|
createIntegration,
|
|
152032
|
+
getHoleFillingCourses,
|
|
152033
|
+
createHoleFillingCourse,
|
|
151588
152034
|
updateIntegration,
|
|
151589
152035
|
deactivateCourse,
|
|
151590
152036
|
reactivateCourse,
|