@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/cli.js
CHANGED
|
@@ -2517,7 +2517,7 @@ var package_default;
|
|
|
2517
2517
|
var init_package = __esm(() => {
|
|
2518
2518
|
package_default = {
|
|
2519
2519
|
name: "@playcademy/sandbox",
|
|
2520
|
-
version: "0.12.2-beta.
|
|
2520
|
+
version: "0.12.2-beta.3",
|
|
2521
2521
|
description: "Local development server for Playcademy game development",
|
|
2522
2522
|
type: "module",
|
|
2523
2523
|
exports: {
|
|
@@ -14774,6 +14774,7 @@ var init_schema = __esm(() => {
|
|
|
14774
14774
|
assessmentArtifactBucket: stringType().optional(),
|
|
14775
14775
|
cdnOrigin: stringType().url().startsWith("https://").optional(),
|
|
14776
14776
|
queueIngressSecret: stringType().optional(),
|
|
14777
|
+
embedAdminToken: stringType().optional(),
|
|
14777
14778
|
secretsManifestPepper: stringType().optional()
|
|
14778
14779
|
}).superRefine((config, ctx) => {
|
|
14779
14780
|
if (config.isLocal && !config.baseUrl) {
|
|
@@ -14940,6 +14941,17 @@ var init_table_utils = __esm(() => {
|
|
|
14940
14941
|
});
|
|
14941
14942
|
|
|
14942
14943
|
// ../../node_modules/.bun/drizzle-orm@0.45.3+c063d24bf874cbdc/node_modules/drizzle-orm/pg-core/foreign-keys.js
|
|
14944
|
+
function foreignKey(config) {
|
|
14945
|
+
function mappedConfig() {
|
|
14946
|
+
const { name: name2, columns, foreignColumns } = config;
|
|
14947
|
+
return {
|
|
14948
|
+
name: name2,
|
|
14949
|
+
columns,
|
|
14950
|
+
foreignColumns
|
|
14951
|
+
};
|
|
14952
|
+
}
|
|
14953
|
+
return new ForeignKeyBuilder(mappedConfig);
|
|
14954
|
+
}
|
|
14943
14955
|
var ForeignKeyBuilder, ForeignKey;
|
|
14944
14956
|
var init_foreign_keys = __esm(() => {
|
|
14945
14957
|
init_entity();
|
|
@@ -17812,6 +17824,16 @@ function ilike(column, value) {
|
|
|
17812
17824
|
function notIlike(column, value) {
|
|
17813
17825
|
return sql`${column} not ilike ${value}`;
|
|
17814
17826
|
}
|
|
17827
|
+
function arrayContains(column, values) {
|
|
17828
|
+
if (Array.isArray(values)) {
|
|
17829
|
+
if (values.length === 0) {
|
|
17830
|
+
throw new Error("arrayContains requires at least one value");
|
|
17831
|
+
}
|
|
17832
|
+
const array = sql`${bindIfParam(values, column)}`;
|
|
17833
|
+
return sql`${column} @> ${array}`;
|
|
17834
|
+
}
|
|
17835
|
+
return sql`${column} @> ${bindIfParam(values, column)}`;
|
|
17836
|
+
}
|
|
17815
17837
|
var eq = (left, right) => {
|
|
17816
17838
|
return sql`${left} = ${bindIfParam(right, left)}`;
|
|
17817
17839
|
}, ne = (left, right) => {
|
|
@@ -20742,7 +20764,7 @@ var init_table6 = __esm(() => {
|
|
|
20742
20764
|
});
|
|
20743
20765
|
|
|
20744
20766
|
// ../data/src/domains/timeback/table.ts
|
|
20745
|
-
var gameTimebackIntegrationStatusEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
20767
|
+
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrationKindEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
20746
20768
|
var init_table7 = __esm(() => {
|
|
20747
20769
|
init_drizzle_orm();
|
|
20748
20770
|
init_pg_core();
|
|
@@ -20753,6 +20775,10 @@ var init_table7 = __esm(() => {
|
|
|
20753
20775
|
"active",
|
|
20754
20776
|
"deactivated"
|
|
20755
20777
|
]);
|
|
20778
|
+
gameTimebackIntegrationKindEnum = pgEnum("game_timeback_integration_kind", [
|
|
20779
|
+
"base",
|
|
20780
|
+
"hole_filling"
|
|
20781
|
+
]);
|
|
20756
20782
|
gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", ASSESSMENT_PURPOSES);
|
|
20757
20783
|
gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
|
|
20758
20784
|
"draft",
|
|
@@ -20766,6 +20792,8 @@ var init_table7 = __esm(() => {
|
|
|
20766
20792
|
grade: integer("grade").notNull(),
|
|
20767
20793
|
subject: text("subject").notNull(),
|
|
20768
20794
|
status: gameTimebackIntegrationStatusEnum("status"),
|
|
20795
|
+
kind: gameTimebackIntegrationKindEnum("kind").notNull().default("base"),
|
|
20796
|
+
baseIntegrationId: uuid("base_integration_id"),
|
|
20769
20797
|
totalXp: integer("total_xp"),
|
|
20770
20798
|
lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
|
|
20771
20799
|
deactivatedAt: timestamp("deactivated_at", { withTimezone: true }),
|
|
@@ -20773,7 +20801,19 @@ var init_table7 = __esm(() => {
|
|
|
20773
20801
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
20774
20802
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
20775
20803
|
}, (table) => [
|
|
20776
|
-
uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table.gameId, table.grade, table.subject).where(sql`${table.status} IS DISTINCT FROM 'deactivated'`)
|
|
20804
|
+
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'`),
|
|
20805
|
+
uniqueIndex("game_timeback_integrations_hole_filling_course_idx").on(table.gameId, table.courseId).where(sql`${table.kind} = 'hole_filling'`),
|
|
20806
|
+
index("game_timeback_integrations_base_integration_idx").on(table.baseIntegrationId),
|
|
20807
|
+
foreignKey({
|
|
20808
|
+
name: "game_timeback_integrations_base_integration_id_fk",
|
|
20809
|
+
columns: [table.baseIntegrationId],
|
|
20810
|
+
foreignColumns: [table.id]
|
|
20811
|
+
}),
|
|
20812
|
+
check("game_timeback_integrations_kind_base_check", sql`(
|
|
20813
|
+
(${table.kind}::text = 'base' AND ${table.baseIntegrationId} IS NULL)
|
|
20814
|
+
OR
|
|
20815
|
+
(${table.kind}::text = 'hole_filling' AND ${table.baseIntegrationId} IS NOT NULL)
|
|
20816
|
+
)`)
|
|
20777
20817
|
]);
|
|
20778
20818
|
gameTimebackAssessmentTests = pgTable("game_timeback_assessment_tests", {
|
|
20779
20819
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -20855,7 +20895,7 @@ var init_table7 = __esm(() => {
|
|
|
20855
20895
|
});
|
|
20856
20896
|
|
|
20857
20897
|
// ../data/src/domains/embed/table.ts
|
|
20858
|
-
var embedPartners,
|
|
20898
|
+
var embedPartners, embedLaunches, embedLaunchesRelations;
|
|
20859
20899
|
var init_table8 = __esm(() => {
|
|
20860
20900
|
init_drizzle_orm();
|
|
20861
20901
|
init_pg_core();
|
|
@@ -20870,15 +20910,6 @@ var init_table8 = __esm(() => {
|
|
|
20870
20910
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
20871
20911
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
20872
20912
|
});
|
|
20873
|
-
embedPartnerGames = pgTable("embed_partner_games", {
|
|
20874
|
-
id: uuid("id").primaryKey().defaultRandom(),
|
|
20875
|
-
partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "cascade" }),
|
|
20876
|
-
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
20877
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
20878
|
-
}, (table) => [
|
|
20879
|
-
uniqueIndex("embed_partner_games_partner_game_idx").on(table.partnerId, table.gameId),
|
|
20880
|
-
index("embed_partner_games_game_idx").on(table.gameId)
|
|
20881
|
-
]);
|
|
20882
20913
|
embedLaunches = pgTable("embed_launches", {
|
|
20883
20914
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
20884
20915
|
partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
|
|
@@ -20897,19 +20928,6 @@ var init_table8 = __esm(() => {
|
|
|
20897
20928
|
index("embed_launches_user_idx").on(table.userId),
|
|
20898
20929
|
index("embed_launches_game_idx").on(table.gameId)
|
|
20899
20930
|
]);
|
|
20900
|
-
embedPartnersRelations = relations(embedPartners, ({ many }) => ({
|
|
20901
|
-
games: many(embedPartnerGames)
|
|
20902
|
-
}));
|
|
20903
|
-
embedPartnerGamesRelations = relations(embedPartnerGames, ({ one }) => ({
|
|
20904
|
-
partner: one(embedPartners, {
|
|
20905
|
-
fields: [embedPartnerGames.partnerId],
|
|
20906
|
-
references: [embedPartners.id]
|
|
20907
|
-
}),
|
|
20908
|
-
game: one(games, {
|
|
20909
|
-
fields: [embedPartnerGames.gameId],
|
|
20910
|
-
references: [games.id]
|
|
20911
|
-
})
|
|
20912
|
-
}));
|
|
20913
20931
|
embedLaunchesRelations = relations(embedLaunches, ({ one }) => ({
|
|
20914
20932
|
partner: one(embedPartners, {
|
|
20915
20933
|
fields: [embedLaunches.partnerId],
|
|
@@ -20940,10 +20958,7 @@ __export(exports_tables_index, {
|
|
|
20940
20958
|
developerStatusEnum: () => developerStatusEnum,
|
|
20941
20959
|
embedLaunches: () => embedLaunches,
|
|
20942
20960
|
embedLaunchesRelations: () => embedLaunchesRelations,
|
|
20943
|
-
embedPartnerGames: () => embedPartnerGames,
|
|
20944
|
-
embedPartnerGamesRelations: () => embedPartnerGamesRelations,
|
|
20945
20961
|
embedPartners: () => embedPartners,
|
|
20946
|
-
embedPartnersRelations: () => embedPartnersRelations,
|
|
20947
20962
|
gameCustomHostnames: () => gameCustomHostnames,
|
|
20948
20963
|
gameDashboardUserRoleEnum: () => gameDashboardUserRoleEnum,
|
|
20949
20964
|
gameDashboardUsers: () => gameDashboardUsers,
|
|
@@ -20962,6 +20977,7 @@ __export(exports_tables_index, {
|
|
|
20962
20977
|
gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
|
|
20963
20978
|
gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
|
|
20964
20979
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
20980
|
+
gameTimebackIntegrationKindEnum: () => gameTimebackIntegrationKindEnum,
|
|
20965
20981
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
20966
20982
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
20967
20983
|
gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
|
|
@@ -22869,10 +22885,19 @@ var init_helpers = __esm(() => {
|
|
|
22869
22885
|
});
|
|
22870
22886
|
|
|
22871
22887
|
// ../data/src/domains/timeback/helpers.ts
|
|
22888
|
+
function isBaseGameTimebackIntegration() {
|
|
22889
|
+
return sql`${gameTimebackIntegrations.kind} = ${BASE_GAME_TIMEBACK_INTEGRATION_KIND}`;
|
|
22890
|
+
}
|
|
22891
|
+
function isBaseGameTimebackIntegrationRow(integration) {
|
|
22892
|
+
return integration.kind === BASE_GAME_TIMEBACK_INTEGRATION_KIND;
|
|
22893
|
+
}
|
|
22872
22894
|
function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
|
|
22873
22895
|
return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
|
|
22874
22896
|
}
|
|
22875
|
-
|
|
22897
|
+
function isActiveBaseGameTimebackIntegration() {
|
|
22898
|
+
return sql`(${isActiveGameTimebackIntegrationStatus()} AND ${isBaseGameTimebackIntegration()})`;
|
|
22899
|
+
}
|
|
22900
|
+
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";
|
|
22876
22901
|
var init_helpers2 = __esm(() => {
|
|
22877
22902
|
init_drizzle_orm();
|
|
22878
22903
|
init_table7();
|
|
@@ -39449,12 +39474,12 @@ function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
|
|
|
39449
39474
|
function filterEnrollmentsByGame(enrollments, gameId) {
|
|
39450
39475
|
return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _, ...enrollment }) => enrollment);
|
|
39451
39476
|
}
|
|
39452
|
-
function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
39477
|
+
function mapEnrollmentsToUserEnrollments(enrollments, integrations, baseCourseIdByIntegrationId = new Map) {
|
|
39453
39478
|
const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
|
|
39454
39479
|
const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
|
|
39455
39480
|
return integrations.map((integration) => {
|
|
39456
39481
|
const enrollment = enrollmentByCourse.get(integration.courseId);
|
|
39457
|
-
|
|
39482
|
+
const userEnrollment = {
|
|
39458
39483
|
gameId: integration.gameId,
|
|
39459
39484
|
grade: integration.grade,
|
|
39460
39485
|
subject: integration.subject,
|
|
@@ -39462,6 +39487,17 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
39462
39487
|
orgId: courseToSchool.get(integration.courseId),
|
|
39463
39488
|
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
39464
39489
|
};
|
|
39490
|
+
if (integration.kind !== HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
39491
|
+
return userEnrollment;
|
|
39492
|
+
}
|
|
39493
|
+
const baseCourseId = integration.baseIntegrationId ? baseCourseIdByIntegrationId.get(integration.baseIntegrationId) : undefined;
|
|
39494
|
+
return {
|
|
39495
|
+
...userEnrollment,
|
|
39496
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
39497
|
+
...baseCourseId && { baseCourseId },
|
|
39498
|
+
...enrollment?.beginDate !== undefined && { beginDate: enrollment.beginDate },
|
|
39499
|
+
...enrollment?.metadata && { enrollmentMetadata: enrollment.metadata }
|
|
39500
|
+
};
|
|
39465
39501
|
});
|
|
39466
39502
|
}
|
|
39467
39503
|
function buildGameTimebackSummaries(integrations) {
|
|
@@ -39944,7 +39980,7 @@ var init_game_service = __esm(() => {
|
|
|
39944
39980
|
const db = this.deps.db;
|
|
39945
39981
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
39946
39982
|
columns: { gameId: true, subject: true },
|
|
39947
|
-
where:
|
|
39983
|
+
where: isActiveBaseGameTimebackIntegration(),
|
|
39948
39984
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
39949
39985
|
});
|
|
39950
39986
|
const subjectMap = {};
|
|
@@ -39968,9 +40004,7 @@ var init_game_service = __esm(() => {
|
|
|
39968
40004
|
}
|
|
39969
40005
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
39970
40006
|
columns: { gameId: true, subject: true, status: true },
|
|
39971
|
-
...accessibleGameIds
|
|
39972
|
-
where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
|
|
39973
|
-
},
|
|
40007
|
+
where: and(accessibleGameIds ? inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds]) : undefined, isBaseGameTimebackIntegration()),
|
|
39974
40008
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
39975
40009
|
});
|
|
39976
40010
|
return buildGameTimebackSummaries(integrations);
|
|
@@ -50401,7 +50435,7 @@ function rejectSystemManagedReview(input, context) {
|
|
|
50401
50435
|
});
|
|
50402
50436
|
}
|
|
50403
50437
|
}
|
|
50404
|
-
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;
|
|
50438
|
+
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;
|
|
50405
50439
|
var init_schemas4 = __esm(() => {
|
|
50406
50440
|
init_esm();
|
|
50407
50441
|
init_src();
|
|
@@ -50437,6 +50471,26 @@ var init_schemas4 = __esm(() => {
|
|
|
50437
50471
|
masterableUnits: numberType().int().nonnegative(),
|
|
50438
50472
|
level: stringType().trim().min(1).optional()
|
|
50439
50473
|
});
|
|
50474
|
+
RESERVED_TIMEBACK_COURSE_SOURCED_IDS = new Set([
|
|
50475
|
+
"hole-filling",
|
|
50476
|
+
"removed",
|
|
50477
|
+
"components",
|
|
50478
|
+
"component-resources",
|
|
50479
|
+
"structure"
|
|
50480
|
+
]);
|
|
50481
|
+
TimebackCourseSourcedIdSchema = stringType().trim().min(3).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
|
|
50482
|
+
message: "Use letters, numbers, dots, dashes, or underscores, starting with a letter or number"
|
|
50483
|
+
}).refine((value) => !RESERVED_TIMEBACK_COURSE_SOURCED_IDS.has(value.toLowerCase()), {
|
|
50484
|
+
message: "This course ID is reserved"
|
|
50485
|
+
});
|
|
50486
|
+
CreateGameTimebackHoleFillingCourseRequestSchema = objectType({
|
|
50487
|
+
baseCourseId: stringType().trim().min(1),
|
|
50488
|
+
title: stringType().trim().min(1),
|
|
50489
|
+
courseCode: stringType().trim().min(1),
|
|
50490
|
+
totalXp: numberType().int().positive(),
|
|
50491
|
+
masterableUnits: numberType().int().nonnegative(),
|
|
50492
|
+
courseId: TimebackCourseSourcedIdSchema.optional()
|
|
50493
|
+
});
|
|
50440
50494
|
TimebackActivityDataSchema = objectType({
|
|
50441
50495
|
activityId: stringType().min(1),
|
|
50442
50496
|
activityName: stringType().optional(),
|
|
@@ -50445,6 +50499,7 @@ var init_schemas4 = __esm(() => {
|
|
|
50445
50499
|
appName: stringType().optional(),
|
|
50446
50500
|
sensorUrl: stringType().url().optional(),
|
|
50447
50501
|
courseId: stringType().min(1).optional(),
|
|
50502
|
+
enrollmentId: stringType().min(1).optional(),
|
|
50448
50503
|
courseName: stringType().optional(),
|
|
50449
50504
|
studentEmail: stringType().email().optional()
|
|
50450
50505
|
});
|
|
@@ -50768,21 +50823,20 @@ function isCanonicalOrigin(value) {
|
|
|
50768
50823
|
}
|
|
50769
50824
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
50770
50825
|
}
|
|
50771
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema,
|
|
50826
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
50772
50827
|
var init_schemas5 = __esm(() => {
|
|
50773
50828
|
init_esm();
|
|
50774
50829
|
LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
|
|
50775
50830
|
EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
|
|
50776
50831
|
message: "Origin must be a bare https origin like https://example.com, with no path or slash"
|
|
50777
50832
|
});
|
|
50778
|
-
|
|
50779
|
-
|
|
50780
|
-
|
|
50781
|
-
|
|
50782
|
-
|
|
50783
|
-
|
|
50833
|
+
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
50834
|
+
EnsureEmbedPartnerSchema = objectType({
|
|
50835
|
+
name: stringType().trim().min(1).max(100)
|
|
50836
|
+
}).strict();
|
|
50837
|
+
AddEmbedPartnerOriginSchema = objectType({
|
|
50838
|
+
origin: EmbedOriginSchema
|
|
50784
50839
|
}).strict();
|
|
50785
|
-
UpdateEmbedPartnerSchema = CreateEmbedPartnerSchema.omit({ clientId: true }).partial();
|
|
50786
50840
|
CreateEmbedLaunchSchema = objectType({
|
|
50787
50841
|
timebackId: stringType().trim().min(1).max(200),
|
|
50788
50842
|
gameSlug: stringType().trim().min(1).max(100),
|
|
@@ -77940,6 +77994,7 @@ class CourseAssessments {
|
|
|
77940
77994
|
}
|
|
77941
77995
|
function buildCoursePayload(config) {
|
|
77942
77996
|
return {
|
|
77997
|
+
...config.course.sourcedId && { sourcedId: config.course.sourcedId },
|
|
77943
77998
|
status: "active",
|
|
77944
77999
|
title: config.course.title,
|
|
77945
78000
|
courseCode: config.course.courseCode,
|
|
@@ -77986,6 +78041,16 @@ function buildComponentResourcePayload(config, sourcedIds) {
|
|
|
77986
78041
|
lessonType: config.componentResource.lessonType
|
|
77987
78042
|
};
|
|
77988
78043
|
}
|
|
78044
|
+
async function findOrUndefined(request) {
|
|
78045
|
+
try {
|
|
78046
|
+
return await request;
|
|
78047
|
+
} catch (error) {
|
|
78048
|
+
if (isApiError(error) && error.statusCode === 404) {
|
|
78049
|
+
return;
|
|
78050
|
+
}
|
|
78051
|
+
throw error;
|
|
78052
|
+
}
|
|
78053
|
+
}
|
|
77989
78054
|
|
|
77990
78055
|
class CourseIntegration {
|
|
77991
78056
|
core;
|
|
@@ -78024,6 +78089,32 @@ class CourseIntegration {
|
|
|
78024
78089
|
...verboseData && { verboseData }
|
|
78025
78090
|
};
|
|
78026
78091
|
}
|
|
78092
|
+
async findCourse(courseId) {
|
|
78093
|
+
return findOrUndefined(this.core.api.oneroster.courses.get(courseId));
|
|
78094
|
+
}
|
|
78095
|
+
async resumeSetup(courseId, config) {
|
|
78096
|
+
const oneroster = this.core.api.oneroster;
|
|
78097
|
+
const sourcedIds = deriveSourcedIds(courseId);
|
|
78098
|
+
if (!await findOrUndefined(oneroster.courses.getComponent(sourcedIds.component))) {
|
|
78099
|
+
await oneroster.courses.createComponent(buildComponentPayload(config, sourcedIds));
|
|
78100
|
+
addEvent("timeback.component_created", {
|
|
78101
|
+
"app.timeback.component_id": sourcedIds.component
|
|
78102
|
+
});
|
|
78103
|
+
}
|
|
78104
|
+
if (!await findOrUndefined(oneroster.resources.get(sourcedIds.resource))) {
|
|
78105
|
+
await oneroster.resources.create(buildResourcePayload(config, sourcedIds.resource));
|
|
78106
|
+
addEvent("timeback.resource_created", {
|
|
78107
|
+
"app.timeback.resource_id": sourcedIds.resource
|
|
78108
|
+
});
|
|
78109
|
+
}
|
|
78110
|
+
if (!await findOrUndefined(oneroster.courses.getComponentResource(sourcedIds.componentResource))) {
|
|
78111
|
+
await oneroster.courses.createComponentResource(buildComponentResourcePayload(config, sourcedIds));
|
|
78112
|
+
addEvent("timeback.component_resource_link_created", {
|
|
78113
|
+
"app.timeback.component_resource_id": sourcedIds.componentResource
|
|
78114
|
+
});
|
|
78115
|
+
}
|
|
78116
|
+
await this.update(courseId, config);
|
|
78117
|
+
}
|
|
78027
78118
|
async update(courseId, config) {
|
|
78028
78119
|
const oneroster = this.core.api.oneroster;
|
|
78029
78120
|
const sourcedIds = deriveSourcedIds(courseId);
|
|
@@ -78163,6 +78254,8 @@ function createCourseNamespace(core) {
|
|
|
78163
78254
|
update: (courseId, config) => integration.update(courseId, config),
|
|
78164
78255
|
verify: (courseId) => integration.verify(courseId),
|
|
78165
78256
|
getConfig: (courseId) => integration.getConfig(courseId),
|
|
78257
|
+
findCourse: (courseId) => integration.findCourse(courseId),
|
|
78258
|
+
resumeSetup: (courseId, config) => integration.resumeSetup(courseId, config),
|
|
78166
78259
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
78167
78260
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
78168
78261
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
@@ -78247,7 +78340,9 @@ class RosterEnrollments {
|
|
|
78247
78340
|
status: "active",
|
|
78248
78341
|
grades,
|
|
78249
78342
|
subjects,
|
|
78250
|
-
school: enrollment.school
|
|
78343
|
+
school: enrollment.school,
|
|
78344
|
+
beginDate: enrollment.beginDate,
|
|
78345
|
+
...enrollment.metadata && { metadata: enrollment.metadata }
|
|
78251
78346
|
};
|
|
78252
78347
|
});
|
|
78253
78348
|
this.core.caches.enrollments.set(studentId, enrollments);
|
|
@@ -89051,7 +89146,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
89051
89146
|
}) {
|
|
89052
89147
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
89053
89148
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
89054
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
89149
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
89055
89150
|
});
|
|
89056
89151
|
const matchingIntegrations = new Map;
|
|
89057
89152
|
for (const integration of integrations) {
|
|
@@ -89440,7 +89535,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
89440
89535
|
}) {
|
|
89441
89536
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
89442
89537
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
89443
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
89538
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
89444
89539
|
});
|
|
89445
89540
|
const orderedIntegrations = integrations.toSorted((left, right) => {
|
|
89446
89541
|
const subjectDifference = left.subject.localeCompare(right.subject);
|
|
@@ -89519,7 +89614,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
89519
89614
|
async candidateIntegrations(gameId, studentId, input, db = this.deps.db) {
|
|
89520
89615
|
const filters = [
|
|
89521
89616
|
eq(gameTimebackIntegrations.gameId, gameId),
|
|
89522
|
-
|
|
89617
|
+
isActiveBaseGameTimebackIntegration()
|
|
89523
89618
|
];
|
|
89524
89619
|
if (input.subject) {
|
|
89525
89620
|
filters.push(eq(gameTimebackIntegrations.subject, input.subject));
|
|
@@ -92419,6 +92514,22 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
92419
92514
|
await init_errors8();
|
|
92420
92515
|
});
|
|
92421
92516
|
|
|
92517
|
+
// ../api-core/src/utils/timeback-attribution.util.ts
|
|
92518
|
+
function resolveRuntimeAttribution(base, holeFilling, requestedCourseId, recordedCourseId) {
|
|
92519
|
+
const recorded = [base, ...holeFilling].find((integration) => integration.courseId === recordedCourseId);
|
|
92520
|
+
if (recorded) {
|
|
92521
|
+
return { integration: recorded, requestedCourseIdStatus: "pinned_by_run" };
|
|
92522
|
+
}
|
|
92523
|
+
if (requestedCourseId === undefined) {
|
|
92524
|
+
return { integration: base, requestedCourseIdStatus: "absent" };
|
|
92525
|
+
}
|
|
92526
|
+
if (requestedCourseId === base.courseId) {
|
|
92527
|
+
return { integration: base, requestedCourseIdStatus: "accepted" };
|
|
92528
|
+
}
|
|
92529
|
+
const requestedHoleFilling = holeFilling.find((integration) => integration.courseId === requestedCourseId);
|
|
92530
|
+
return requestedHoleFilling ? { integration: requestedHoleFilling, requestedCourseIdStatus: "accepted_hole_filling" } : { integration: base, requestedCourseIdStatus: "ignored_mismatch" };
|
|
92531
|
+
}
|
|
92532
|
+
|
|
92422
92533
|
// ../api-core/src/utils/timeback-create-integration.util.ts
|
|
92423
92534
|
function buildTimebackBaseConfigFromExistingConfig(config) {
|
|
92424
92535
|
return {
|
|
@@ -92461,6 +92572,50 @@ function timebackConfigMatchesCreateIntegrationRequest(config, request) {
|
|
|
92461
92572
|
const requestedLevel = request.level ?? deriveTimebackCourseLevelFromGrade(request.grade);
|
|
92462
92573
|
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;
|
|
92463
92574
|
}
|
|
92575
|
+
function buildHoleFillingCourseConfig(baseConfig, request) {
|
|
92576
|
+
const baseCourseMetadata = isRecord4(baseConfig.course.metadata) ? baseConfig.course.metadata : {};
|
|
92577
|
+
const baseMetrics = isRecord4(baseCourseMetadata.metrics) ? baseCourseMetadata.metrics : {};
|
|
92578
|
+
const baseResourceMetadata = isRecord4(baseConfig.resource.metadata) ? baseConfig.resource.metadata : {};
|
|
92579
|
+
const basePlaycademy = isPlaycademyResourceMetadata(baseResourceMetadata.playcademy) ? baseResourceMetadata.playcademy : undefined;
|
|
92580
|
+
return {
|
|
92581
|
+
organization: baseConfig.organization,
|
|
92582
|
+
course: {
|
|
92583
|
+
...baseConfig.course,
|
|
92584
|
+
...request.courseId && { sourcedId: request.courseId },
|
|
92585
|
+
title: request.title,
|
|
92586
|
+
courseCode: request.courseCode,
|
|
92587
|
+
metadata: {
|
|
92588
|
+
...baseCourseMetadata,
|
|
92589
|
+
courseType: HOLE_FILLING_COURSE_TYPE,
|
|
92590
|
+
baseCourseSourcedId: request.baseCourseId,
|
|
92591
|
+
metrics: {
|
|
92592
|
+
...baseMetrics,
|
|
92593
|
+
totalXp: request.totalXp,
|
|
92594
|
+
totalLessons: request.masterableUnits,
|
|
92595
|
+
totalGrades: 1
|
|
92596
|
+
}
|
|
92597
|
+
}
|
|
92598
|
+
},
|
|
92599
|
+
component: { ...baseConfig.component, title: `${request.title} Activities` },
|
|
92600
|
+
resource: {
|
|
92601
|
+
...baseConfig.resource,
|
|
92602
|
+
title: `${request.title} Game`,
|
|
92603
|
+
metadata: {
|
|
92604
|
+
...baseResourceMetadata,
|
|
92605
|
+
xp: request.totalXp,
|
|
92606
|
+
playcademy: {
|
|
92607
|
+
...basePlaycademy,
|
|
92608
|
+
mastery: {
|
|
92609
|
+
...basePlaycademy?.mastery,
|
|
92610
|
+
masterableUnits: request.masterableUnits
|
|
92611
|
+
}
|
|
92612
|
+
}
|
|
92613
|
+
}
|
|
92614
|
+
},
|
|
92615
|
+
componentResource: { ...baseConfig.componentResource, title: `${request.title} Lesson` }
|
|
92616
|
+
};
|
|
92617
|
+
}
|
|
92618
|
+
var HOLE_FILLING_COURSE_TYPE = "hole-filling";
|
|
92464
92619
|
var init_timeback_create_integration_util = __esm(() => {
|
|
92465
92620
|
init_constants2();
|
|
92466
92621
|
init_types2();
|
|
@@ -92476,7 +92631,7 @@ async function promoteCompletedCourse({
|
|
|
92476
92631
|
enrollments: prefetchedEnrollments
|
|
92477
92632
|
}) {
|
|
92478
92633
|
const subjectIntegrations = await db.query.gameTimebackIntegrations.findMany({
|
|
92479
|
-
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject),
|
|
92634
|
+
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveBaseGameTimebackIntegration())
|
|
92480
92635
|
});
|
|
92481
92636
|
const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
|
|
92482
92637
|
if (!nextIntegration) {
|
|
@@ -92543,6 +92698,8 @@ function toGameTimebackIntegration(integration) {
|
|
|
92543
92698
|
courseId: integration.courseId,
|
|
92544
92699
|
grade: integration.grade,
|
|
92545
92700
|
subject: integration.subject,
|
|
92701
|
+
kind: integration.kind,
|
|
92702
|
+
baseIntegrationId: integration.baseIntegrationId,
|
|
92546
92703
|
totalXp: integration.totalXp ?? null,
|
|
92547
92704
|
status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
|
|
92548
92705
|
deactivatedAt: integration.deactivatedAt ?? null,
|
|
@@ -92580,6 +92737,9 @@ var init_timeback_removed_integration_util = __esm(() => {
|
|
|
92580
92737
|
|
|
92581
92738
|
// ../api-core/src/services/timeback.service.ts
|
|
92582
92739
|
import { createHash as createHash3 } from "node:crypto";
|
|
92740
|
+
function isUnregisteredHoleFillingOf(course, base) {
|
|
92741
|
+
return course.metadata?.courseType === HOLE_FILLING_COURSE_TYPE && course.metadata.baseCourseSourcedId === base.courseId;
|
|
92742
|
+
}
|
|
92583
92743
|
async function findGameTimebackIntegrationForUpdate(db, condition) {
|
|
92584
92744
|
const [integration] = await db.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
|
|
92585
92745
|
return integration;
|
|
@@ -92629,13 +92789,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
92629
92789
|
static clearInFlightHeartbeatWindow(key) {
|
|
92630
92790
|
this.inFlightHeartbeatWindows.delete(key);
|
|
92631
92791
|
}
|
|
92632
|
-
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
92792
|
+
static buildCompletionCaliperEventId(key, kind, baseCourseId, discriminator = "completion") {
|
|
92633
92793
|
const hash = createHash3("sha256").update([
|
|
92634
92794
|
"playcademy",
|
|
92635
92795
|
"timeback",
|
|
92636
92796
|
kind,
|
|
92637
92797
|
key.gameId,
|
|
92638
|
-
|
|
92798
|
+
baseCourseId,
|
|
92639
92799
|
key.studentId,
|
|
92640
92800
|
key.runId,
|
|
92641
92801
|
key.activityId,
|
|
@@ -92701,11 +92861,14 @@ var init_timeback_service = __esm(async () => {
|
|
|
92701
92861
|
} : null
|
|
92702
92862
|
};
|
|
92703
92863
|
}
|
|
92704
|
-
static async acquireCompletionReservation(db, key) {
|
|
92705
|
-
|
|
92864
|
+
static async acquireCompletionReservation(db, key, otherCourseIds = []) {
|
|
92865
|
+
function reserve() {
|
|
92866
|
+
return otherCourseIds.length > 0 ? TimebackService.reserveCompletionAcrossCourses(db, key, otherCourseIds) : TimebackService.reserveCompletion(db, key);
|
|
92867
|
+
}
|
|
92868
|
+
let reserveOutcome = await reserve();
|
|
92706
92869
|
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
92707
92870
|
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
92708
|
-
reserveOutcome = await
|
|
92871
|
+
reserveOutcome = await reserve();
|
|
92709
92872
|
}
|
|
92710
92873
|
if (reserveOutcome.status === "duplicate") {
|
|
92711
92874
|
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
@@ -92724,8 +92887,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
92724
92887
|
}
|
|
92725
92888
|
return { status: "reserved", token: reserveOutcome.token };
|
|
92726
92889
|
}
|
|
92727
|
-
static async
|
|
92728
|
-
|
|
92890
|
+
static async reserveCompletionAcrossCourses(db, key, otherCourseIds) {
|
|
92891
|
+
return db.transaction(async (tx) => {
|
|
92892
|
+
const database = tx;
|
|
92893
|
+
const runKey = `timeback-completion:${key.gameId.toLowerCase()}:${key.studentId}:${key.runId.toLowerCase()}:${key.activityId}`;
|
|
92894
|
+
await database.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${runKey}, 0))`);
|
|
92895
|
+
const onOtherCourse = await database.query.gameTimebackActivityCompletions.findFirst({
|
|
92896
|
+
columns: { id: true },
|
|
92897
|
+
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]))
|
|
92898
|
+
});
|
|
92899
|
+
if (onOtherCourse) {
|
|
92900
|
+
setAttribute("app.timeback.end_activity_status", "completion_reserved_on_other_course");
|
|
92901
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
92902
|
+
}
|
|
92903
|
+
return this.reserveCompletion(database, key);
|
|
92904
|
+
});
|
|
92905
|
+
}
|
|
92906
|
+
static async runWithCompletionReservation(db, key, emit, awardFromResult, otherCourseIds = []) {
|
|
92907
|
+
const acquired = await this.acquireCompletionReservation(db, key, otherCourseIds);
|
|
92729
92908
|
if (acquired.status === "duplicate") {
|
|
92730
92909
|
return acquired;
|
|
92731
92910
|
}
|
|
@@ -92760,6 +92939,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
92760
92939
|
await db.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
92761
92940
|
}
|
|
92762
92941
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
92942
|
+
return TimebackService.addPlaycademyExtensions(extensions, { resumeId });
|
|
92943
|
+
}
|
|
92944
|
+
static addPlaycademyExtensions(extensions, fields) {
|
|
92763
92945
|
const base = extensions ?? {};
|
|
92764
92946
|
const existingPlaycademy = base.playcademy;
|
|
92765
92947
|
const playcademy = typeof existingPlaycademy === "object" && existingPlaycademy !== null && !Array.isArray(existingPlaycademy) ? existingPlaycademy : {};
|
|
@@ -92767,28 +92949,71 @@ var init_timeback_service = __esm(async () => {
|
|
|
92767
92949
|
...base,
|
|
92768
92950
|
playcademy: {
|
|
92769
92951
|
...playcademy,
|
|
92770
|
-
|
|
92952
|
+
...fields
|
|
92771
92953
|
}
|
|
92772
92954
|
};
|
|
92773
92955
|
}
|
|
92774
|
-
|
|
92775
|
-
|
|
92776
|
-
|
|
92777
|
-
|
|
92778
|
-
|
|
92779
|
-
|
|
92956
|
+
async resolveRuntimeCourse({
|
|
92957
|
+
gameId,
|
|
92958
|
+
studentId,
|
|
92959
|
+
runId,
|
|
92960
|
+
activityData,
|
|
92961
|
+
extensions
|
|
92962
|
+
}) {
|
|
92963
|
+
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
92964
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
92965
|
+
});
|
|
92966
|
+
const base = integrations.find(isBaseGameTimebackIntegrationRow);
|
|
92967
|
+
if (!base) {
|
|
92968
|
+
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
92780
92969
|
}
|
|
92970
|
+
const holeFilling = integrations.filter((integration) => !isBaseGameTimebackIntegrationRow(integration));
|
|
92971
|
+
const recordedCourseId = holeFilling.length > 0 && runId ? await this.findRecordedRunCourse({
|
|
92972
|
+
gameId,
|
|
92973
|
+
studentId,
|
|
92974
|
+
runId,
|
|
92975
|
+
activityId: activityData.activityId,
|
|
92976
|
+
courseIds: integrations.map((i2) => i2.courseId)
|
|
92977
|
+
}) : undefined;
|
|
92978
|
+
const { integration, requestedCourseIdStatus } = resolveRuntimeAttribution(base, holeFilling, activityData.courseId, recordedCourseId);
|
|
92979
|
+
const hintUsed = activityData.courseId === integration.courseId;
|
|
92980
|
+
const enrollmentId = hintUsed ? activityData.enrollmentId : undefined;
|
|
92981
|
+
const isHoleFilling = !isBaseGameTimebackIntegrationRow(integration);
|
|
92781
92982
|
return {
|
|
92782
|
-
|
|
92783
|
-
|
|
92983
|
+
integration,
|
|
92984
|
+
courseId: integration.courseId,
|
|
92985
|
+
baseCourseId: base.courseId,
|
|
92986
|
+
otherCourseIds: holeFilling.length > 0 ? integrations.map((other) => other.courseId).filter((courseId) => courseId !== integration.courseId) : [],
|
|
92987
|
+
courseName: activityData.courseId === undefined || hintUsed ? activityData.courseName : undefined,
|
|
92988
|
+
extensions: isHoleFilling ? TimebackService.addPlaycademyExtensions(extensions, {
|
|
92989
|
+
courseKind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
92990
|
+
baseCourseId: base.courseId,
|
|
92991
|
+
...enrollmentId && { enrollmentId }
|
|
92992
|
+
}) : extensions,
|
|
92784
92993
|
attributes: {
|
|
92785
|
-
"app.timeback.course_id":
|
|
92786
|
-
"app.timeback.resolved_course_id":
|
|
92787
|
-
"app.timeback.requested_course_id":
|
|
92788
|
-
"app.timeback.requested_course_id_status": requestedCourseIdStatus
|
|
92994
|
+
"app.timeback.course_id": integration.courseId,
|
|
92995
|
+
"app.timeback.resolved_course_id": integration.courseId,
|
|
92996
|
+
"app.timeback.requested_course_id": activityData.courseId,
|
|
92997
|
+
"app.timeback.requested_course_id_status": requestedCourseIdStatus,
|
|
92998
|
+
"app.timeback.course_kind": integration.kind,
|
|
92999
|
+
"app.timeback.enrollment_id": enrollmentId
|
|
92789
93000
|
}
|
|
92790
93001
|
};
|
|
92791
93002
|
}
|
|
93003
|
+
async findRecordedRunCourse({
|
|
93004
|
+
gameId,
|
|
93005
|
+
studentId,
|
|
93006
|
+
runId,
|
|
93007
|
+
activityId,
|
|
93008
|
+
courseIds
|
|
93009
|
+
}) {
|
|
93010
|
+
const recorded = await this.deps.db.query.gameTimebackActivityCompletions.findFirst({
|
|
93011
|
+
columns: { courseId: true },
|
|
93012
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, gameId), eq(gameTimebackActivityCompletions.studentId, studentId), eq(gameTimebackActivityCompletions.runId, runId), eq(gameTimebackActivityCompletions.activityId, activityId), inArray(gameTimebackActivityCompletions.courseId, courseIds)),
|
|
93013
|
+
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
93014
|
+
});
|
|
93015
|
+
return recorded?.courseId;
|
|
93016
|
+
}
|
|
92792
93017
|
static recordRuntimeContext({
|
|
92793
93018
|
operation,
|
|
92794
93019
|
studentId,
|
|
@@ -92986,13 +93211,29 @@ var init_timeback_service = __esm(async () => {
|
|
|
92986
93211
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
92987
93212
|
where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
|
|
92988
93213
|
});
|
|
92989
|
-
return mapEnrollmentsToUserEnrollments(enrollments, integrations);
|
|
93214
|
+
return mapEnrollmentsToUserEnrollments(enrollments, integrations, await this.loadBaseCourseIds(integrations));
|
|
92990
93215
|
} catch (error) {
|
|
92991
93216
|
setAttribute("app.timeback.enrollment_fetch", "failed");
|
|
92992
93217
|
setAttribute("app.timeback.enrollment_fetch_error", errorMessage(error));
|
|
92993
93218
|
return [];
|
|
92994
93219
|
}
|
|
92995
93220
|
}
|
|
93221
|
+
async loadBaseCourseIds(integrations) {
|
|
93222
|
+
const courseIdById = new Map(integrations.map((row) => [row.id, row.courseId]));
|
|
93223
|
+
const missingBaseIds = [
|
|
93224
|
+
...new Set(integrations.map((row) => row.baseIntegrationId).filter((id) => id !== null && !courseIdById.has(id)))
|
|
93225
|
+
];
|
|
93226
|
+
if (missingBaseIds.length > 0) {
|
|
93227
|
+
const baseRows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93228
|
+
columns: { id: true, courseId: true },
|
|
93229
|
+
where: inArray(gameTimebackIntegrations.id, missingBaseIds)
|
|
93230
|
+
});
|
|
93231
|
+
for (const row of baseRows) {
|
|
93232
|
+
courseIdById.set(row.id, row.courseId);
|
|
93233
|
+
}
|
|
93234
|
+
}
|
|
93235
|
+
return courseIdById;
|
|
93236
|
+
}
|
|
92996
93237
|
async setupIntegration(gameId, request, user) {
|
|
92997
93238
|
return this.withClientTelemetry(async () => {
|
|
92998
93239
|
const client = this.requireClient();
|
|
@@ -93003,7 +93244,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93003
93244
|
"app.timeback.course_count": courses.length
|
|
93004
93245
|
});
|
|
93005
93246
|
const existing = await db.query.gameTimebackIntegrations.findMany({
|
|
93006
|
-
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
93247
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration())
|
|
93007
93248
|
});
|
|
93008
93249
|
const integrations = [];
|
|
93009
93250
|
const verboseData = [];
|
|
@@ -93151,13 +93392,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
93151
93392
|
"app.timeback.subject": request.subject
|
|
93152
93393
|
});
|
|
93153
93394
|
const activeConflict = await db.query.gameTimebackIntegrations.findFirst({
|
|
93154
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject),
|
|
93395
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveBaseGameTimebackIntegration())
|
|
93155
93396
|
});
|
|
93156
93397
|
if (activeConflict) {
|
|
93157
93398
|
throw new ConflictError(`A TimeBack integration already exists for ${request.subject} Grade ${request.grade}`);
|
|
93158
93399
|
}
|
|
93159
93400
|
const removedCandidates = await db.query.gameTimebackIntegrations.findMany({
|
|
93160
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))
|
|
93401
|
+
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())
|
|
93161
93402
|
});
|
|
93162
93403
|
for (const candidate of removedCandidates) {
|
|
93163
93404
|
const config = await client.course.getConfig(candidate.courseId).catch((error) => {
|
|
@@ -93195,10 +93436,10 @@ var init_timeback_service = __esm(async () => {
|
|
|
93195
93436
|
}
|
|
93196
93437
|
}
|
|
93197
93438
|
const reference = await db.query.gameTimebackIntegrations.findFirst({
|
|
93198
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93439
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration()),
|
|
93199
93440
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
93200
93441
|
}) ?? await db.query.gameTimebackIntegrations.findFirst({
|
|
93201
|
-
where: eq(gameTimebackIntegrations.gameId, gameId),
|
|
93442
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration()),
|
|
93202
93443
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
93203
93444
|
});
|
|
93204
93445
|
if (!reference) {
|
|
@@ -93228,10 +93469,77 @@ var init_timeback_service = __esm(async () => {
|
|
|
93228
93469
|
return integration;
|
|
93229
93470
|
});
|
|
93230
93471
|
}
|
|
93472
|
+
async createHoleFillingCourse(gameId, user, request) {
|
|
93473
|
+
return this.withClientTelemetry(async () => {
|
|
93474
|
+
const client = this.requireClient();
|
|
93475
|
+
const db = this.deps.db;
|
|
93476
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93477
|
+
TimebackService.recordIntegrationOperation("create_hole_filling_course", {
|
|
93478
|
+
"app.timeback.base_course_id": request.baseCourseId,
|
|
93479
|
+
"app.timeback.custom_course_id": Boolean(request.courseId)
|
|
93480
|
+
});
|
|
93481
|
+
const integration = await db.transaction(async (tx) => {
|
|
93482
|
+
const database = tx;
|
|
93483
|
+
const base = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, request.baseCourseId), isActiveBaseGameTimebackIntegration()));
|
|
93484
|
+
if (!base) {
|
|
93485
|
+
throw new NotFoundError("Active base course", `${gameId}:${request.baseCourseId}`);
|
|
93486
|
+
}
|
|
93487
|
+
const config = buildHoleFillingCourseConfig(await client.course.getConfig(base.courseId), request);
|
|
93488
|
+
let courseId = request.courseId;
|
|
93489
|
+
if (courseId) {
|
|
93490
|
+
const takenLocally = await database.query.gameTimebackIntegrations.findFirst({
|
|
93491
|
+
columns: { id: true },
|
|
93492
|
+
where: eq(gameTimebackIntegrations.courseId, courseId)
|
|
93493
|
+
});
|
|
93494
|
+
const remote = takenLocally ? undefined : await client.course.findCourse(courseId);
|
|
93495
|
+
if (takenLocally || remote && !isUnregisteredHoleFillingOf(remote, base)) {
|
|
93496
|
+
throw new ConflictError(`A Timeback course with ID "${courseId}" already exists`);
|
|
93497
|
+
}
|
|
93498
|
+
if (remote) {
|
|
93499
|
+
setAttribute("app.timeback.course_create_mode", "resumed");
|
|
93500
|
+
await client.course.resumeSetup(courseId, config);
|
|
93501
|
+
} else {
|
|
93502
|
+
courseId = undefined;
|
|
93503
|
+
}
|
|
93504
|
+
}
|
|
93505
|
+
if (!courseId) {
|
|
93506
|
+
const created = await client.course.setup(config);
|
|
93507
|
+
courseId = created.courseId;
|
|
93508
|
+
}
|
|
93509
|
+
setAttribute("app.timeback.course_id", courseId);
|
|
93510
|
+
const [inserted] = await database.insert(gameTimebackIntegrations).values({
|
|
93511
|
+
gameId,
|
|
93512
|
+
courseId,
|
|
93513
|
+
grade: base.grade,
|
|
93514
|
+
subject: base.subject,
|
|
93515
|
+
totalXp: request.totalXp,
|
|
93516
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
93517
|
+
baseIntegrationId: base.id
|
|
93518
|
+
}).returning();
|
|
93519
|
+
return inserted;
|
|
93520
|
+
});
|
|
93521
|
+
if (!integration) {
|
|
93522
|
+
throw new InternalError("Hole-filling course creation did not return an integration");
|
|
93523
|
+
}
|
|
93524
|
+
return toGameTimebackIntegration(integration);
|
|
93525
|
+
});
|
|
93526
|
+
}
|
|
93527
|
+
async getHoleFillingCourses(gameId, user) {
|
|
93528
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
93529
|
+
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93530
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.kind, HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND), isActiveGameTimebackIntegrationStatus()),
|
|
93531
|
+
orderBy: (table, { asc }) => [
|
|
93532
|
+
asc(table.subject),
|
|
93533
|
+
asc(table.grade),
|
|
93534
|
+
asc(table.createdAt)
|
|
93535
|
+
]
|
|
93536
|
+
});
|
|
93537
|
+
return rows.map((row) => toGameTimebackIntegration(row));
|
|
93538
|
+
}
|
|
93231
93539
|
async getIntegrations(gameId, user) {
|
|
93232
93540
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
93233
93541
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93234
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93542
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
93235
93543
|
});
|
|
93236
93544
|
return rows.map((row) => toGameTimebackIntegration(row));
|
|
93237
93545
|
}
|
|
@@ -93240,7 +93548,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93240
93548
|
const client = this.requireClient();
|
|
93241
93549
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
93242
93550
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93243
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
|
|
93551
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration()),
|
|
93244
93552
|
orderBy: (table, { asc }) => [asc(table.subject), asc(table.grade)]
|
|
93245
93553
|
});
|
|
93246
93554
|
setAttribute("app.timeback.removed_integration_count", rows.length);
|
|
@@ -93271,6 +93579,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
93271
93579
|
if (!integration) {
|
|
93272
93580
|
throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
|
|
93273
93581
|
}
|
|
93582
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
93583
|
+
throw new ConflictError("This is a hole-filling course and may be referenced by Timeback, so it can't be removed.");
|
|
93584
|
+
}
|
|
93585
|
+
const activeHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
93586
|
+
columns: { id: true },
|
|
93587
|
+
where: and(eq(gameTimebackIntegrations.baseIntegrationId, integration.id), isActiveGameTimebackIntegrationStatus())
|
|
93588
|
+
});
|
|
93589
|
+
if (activeHoleFilling) {
|
|
93590
|
+
setAttribute("app.timeback.removal_blocked", "hole_filling_courses");
|
|
93591
|
+
throw new ConflictError("This course has hole-filling courses that may be referenced by Timeback, so it can't be removed.");
|
|
93592
|
+
}
|
|
93274
93593
|
const activeEnrollments = await client.roster.getCourseRoster(courseId, {
|
|
93275
93594
|
role: "student",
|
|
93276
93595
|
includeUsers: false
|
|
@@ -93308,11 +93627,18 @@ var init_timeback_service = __esm(async () => {
|
|
|
93308
93627
|
if (!integration) {
|
|
93309
93628
|
throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
|
|
93310
93629
|
}
|
|
93311
|
-
|
|
93312
|
-
|
|
93313
|
-
|
|
93314
|
-
|
|
93315
|
-
|
|
93630
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
93631
|
+
const base = integration.baseIntegrationId ? await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.id, integration.baseIntegrationId), isActiveBaseGameTimebackIntegration())) : undefined;
|
|
93632
|
+
if (!base) {
|
|
93633
|
+
throw new ConflictError("Restore the base course first: a hole-filling course can only be active beside its base course.");
|
|
93634
|
+
}
|
|
93635
|
+
} else {
|
|
93636
|
+
const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
93637
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveBaseGameTimebackIntegration())
|
|
93638
|
+
});
|
|
93639
|
+
if (activeConflict) {
|
|
93640
|
+
throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
|
|
93641
|
+
}
|
|
93316
93642
|
}
|
|
93317
93643
|
const now = new Date;
|
|
93318
93644
|
const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
|
|
@@ -93351,52 +93677,66 @@ var init_timeback_service = __esm(async () => {
|
|
|
93351
93677
|
TimebackService.recordIntegrationOperation("update_integration", {
|
|
93352
93678
|
"app.timeback.course_id": courseId
|
|
93353
93679
|
});
|
|
93354
|
-
const
|
|
93355
|
-
|
|
93356
|
-
|
|
93357
|
-
|
|
93358
|
-
|
|
93359
|
-
|
|
93360
|
-
|
|
93361
|
-
|
|
93362
|
-
|
|
93363
|
-
|
|
93364
|
-
|
|
93365
|
-
|
|
93366
|
-
|
|
93367
|
-
|
|
93368
|
-
|
|
93369
|
-
|
|
93370
|
-
});
|
|
93371
|
-
if (subject !== integration.subject) {
|
|
93372
|
-
const subjectConflict = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
93373
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus())
|
|
93680
|
+
const updated = await this.deps.db.transaction(async (tx) => {
|
|
93681
|
+
const database = tx;
|
|
93682
|
+
const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
|
|
93683
|
+
if (!integration) {
|
|
93684
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
93685
|
+
}
|
|
93686
|
+
const timebackConfig = await client.course.getConfig(courseId);
|
|
93687
|
+
const liveSubject = timebackConfig.course.subjects[0];
|
|
93688
|
+
const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
|
|
93689
|
+
if (!isTimebackSubject(subject)) {
|
|
93690
|
+
throw new ValidationError(`Invalid subject "${subject}"`);
|
|
93691
|
+
}
|
|
93692
|
+
setAttributes({
|
|
93693
|
+
"app.timeback.grade": integration.grade,
|
|
93694
|
+
"app.timeback.previous_subject": integration.subject,
|
|
93695
|
+
"app.timeback.next_subject": subject
|
|
93374
93696
|
});
|
|
93375
|
-
if (
|
|
93376
|
-
|
|
93697
|
+
if (subject !== integration.subject) {
|
|
93698
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
93699
|
+
throw new ValidationError("A hole-filling course must keep the subject of its base course");
|
|
93700
|
+
}
|
|
93701
|
+
const linkedHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
93702
|
+
columns: { id: true },
|
|
93703
|
+
where: eq(gameTimebackIntegrations.baseIntegrationId, integration.id)
|
|
93704
|
+
});
|
|
93705
|
+
if (linkedHoleFilling) {
|
|
93706
|
+
throw new ValidationError("This course has hole-filling courses, so its subject cannot change");
|
|
93707
|
+
}
|
|
93377
93708
|
}
|
|
93378
|
-
|
|
93379
|
-
|
|
93380
|
-
|
|
93381
|
-
|
|
93382
|
-
|
|
93383
|
-
|
|
93709
|
+
if (subject !== integration.subject) {
|
|
93710
|
+
const subjectConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
93711
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveBaseGameTimebackIntegration())
|
|
93712
|
+
});
|
|
93713
|
+
if (subjectConflict && subjectConflict.id !== integration.id) {
|
|
93714
|
+
throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
|
|
93715
|
+
}
|
|
93716
|
+
}
|
|
93717
|
+
const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
|
|
93718
|
+
const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
|
|
93719
|
+
setAttributes({
|
|
93720
|
+
"app.timeback.total_xp": totalXp,
|
|
93721
|
+
"app.timeback.masterable_units": masterableUnits
|
|
93722
|
+
});
|
|
93723
|
+
await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
|
|
93724
|
+
...patch,
|
|
93725
|
+
subject,
|
|
93726
|
+
totalXp,
|
|
93727
|
+
masterableUnits,
|
|
93728
|
+
grade: integration.grade
|
|
93729
|
+
}));
|
|
93730
|
+
const [row] = await database.update(gameTimebackIntegrations).set({
|
|
93731
|
+
subject,
|
|
93732
|
+
totalXp,
|
|
93733
|
+
updatedAt: new Date
|
|
93734
|
+
}).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
|
|
93735
|
+
if (!row) {
|
|
93736
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
93737
|
+
}
|
|
93738
|
+
return row;
|
|
93384
93739
|
});
|
|
93385
|
-
await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
|
|
93386
|
-
...patch,
|
|
93387
|
-
subject,
|
|
93388
|
-
totalXp,
|
|
93389
|
-
masterableUnits,
|
|
93390
|
-
grade: integration.grade
|
|
93391
|
-
}));
|
|
93392
|
-
const [updated] = await this.deps.db.update(gameTimebackIntegrations).set({
|
|
93393
|
-
subject,
|
|
93394
|
-
totalXp,
|
|
93395
|
-
updatedAt: new Date
|
|
93396
|
-
}).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
|
|
93397
|
-
if (!updated) {
|
|
93398
|
-
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
93399
|
-
}
|
|
93400
93740
|
return toGameTimebackIntegration(updated);
|
|
93401
93741
|
});
|
|
93402
93742
|
}
|
|
@@ -93407,7 +93747,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93407
93747
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93408
93748
|
TimebackService.recordIntegrationOperation("verify_integration");
|
|
93409
93749
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
93410
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93750
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
93411
93751
|
});
|
|
93412
93752
|
if (integrations.length === 0) {
|
|
93413
93753
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -93445,7 +93785,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93445
93785
|
const client = this.requireClient();
|
|
93446
93786
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93447
93787
|
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
93448
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93788
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
93449
93789
|
});
|
|
93450
93790
|
if (!integration) {
|
|
93451
93791
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -93459,17 +93799,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
93459
93799
|
const db = this.deps.db;
|
|
93460
93800
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93461
93801
|
TimebackService.recordIntegrationOperation("delete_integrations");
|
|
93462
|
-
|
|
93463
|
-
|
|
93802
|
+
await db.transaction(async (tx) => {
|
|
93803
|
+
const database = tx;
|
|
93804
|
+
await database.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId)).for("update");
|
|
93805
|
+
const integrations = await database.select().from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
93806
|
+
if (integrations.length === 0) {
|
|
93807
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
93808
|
+
}
|
|
93809
|
+
setAttribute("app.timeback.integration_count", integrations.length);
|
|
93810
|
+
const holeFillingCount = integrations.filter((integration) => integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND).length;
|
|
93811
|
+
if (holeFillingCount > 0) {
|
|
93812
|
+
setAttribute("app.timeback.hole_filling_count", holeFillingCount);
|
|
93813
|
+
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.`);
|
|
93814
|
+
}
|
|
93815
|
+
for (const integration of integrations) {
|
|
93816
|
+
await client.course.cleanup(integration.courseId);
|
|
93817
|
+
}
|
|
93818
|
+
await database.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
93464
93819
|
});
|
|
93465
|
-
if (integrations.length === 0) {
|
|
93466
|
-
throw new NotFoundError("Timeback integration", gameId);
|
|
93467
|
-
}
|
|
93468
|
-
setAttribute("app.timeback.integration_count", integrations.length);
|
|
93469
|
-
for (const integration of integrations) {
|
|
93470
|
-
await client.course.cleanup(integration.courseId);
|
|
93471
|
-
}
|
|
93472
|
-
await db.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
93473
93820
|
});
|
|
93474
93821
|
}
|
|
93475
93822
|
static patchCourseMetadata(metadata2, totalXp, options) {
|
|
@@ -93648,17 +93995,21 @@ var init_timeback_service = __esm(async () => {
|
|
|
93648
93995
|
activityData
|
|
93649
93996
|
});
|
|
93650
93997
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93651
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
93652
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
93653
|
-
});
|
|
93654
|
-
if (!integration) {
|
|
93655
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
93656
|
-
}
|
|
93657
93998
|
const {
|
|
93999
|
+
integration,
|
|
93658
94000
|
courseId: runtimeCourseId,
|
|
94001
|
+
baseCourseId,
|
|
94002
|
+
otherCourseIds,
|
|
93659
94003
|
courseName: runtimeCourseName,
|
|
94004
|
+
extensions: runtimeExtensions,
|
|
93660
94005
|
attributes: courseAttributes
|
|
93661
|
-
} =
|
|
94006
|
+
} = await this.resolveRuntimeCourse({
|
|
94007
|
+
gameId,
|
|
94008
|
+
studentId,
|
|
94009
|
+
runId,
|
|
94010
|
+
activityData,
|
|
94011
|
+
extensions: extensionsWithResumeId
|
|
94012
|
+
});
|
|
93662
94013
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
93663
94014
|
const dedupeKey = runId !== undefined ? {
|
|
93664
94015
|
gameId,
|
|
@@ -93687,12 +94038,12 @@ var init_timeback_service = __esm(async () => {
|
|
|
93687
94038
|
courseId: runtimeCourseId,
|
|
93688
94039
|
courseName: runtimeCourseName,
|
|
93689
94040
|
studentEmail: activityData.studentEmail,
|
|
93690
|
-
extensions:
|
|
94041
|
+
extensions: runtimeExtensions,
|
|
93691
94042
|
...runId ? { runId } : {}
|
|
93692
94043
|
};
|
|
93693
|
-
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
93694
|
-
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
93695
|
-
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
94044
|
+
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion", baseCourseId) : undefined;
|
|
94045
|
+
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", baseCourseId, effectiveResumeId) : undefined;
|
|
94046
|
+
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed", baseCourseId) : undefined;
|
|
93696
94047
|
function emitRecord() {
|
|
93697
94048
|
return client.activity.record(runtimeCourseId, studentId, {
|
|
93698
94049
|
...activityEventContext,
|
|
@@ -93716,7 +94067,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93716
94067
|
xpAwarded: emitted.xpAwarded,
|
|
93717
94068
|
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
93718
94069
|
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
93719
|
-
}));
|
|
94070
|
+
}), otherCourseIds);
|
|
93720
94071
|
if (outcome.status === "duplicate") {
|
|
93721
94072
|
blockedByDedupeGuard = true;
|
|
93722
94073
|
duplicateAward = outcome.award;
|
|
@@ -93764,6 +94115,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93764
94115
|
return {
|
|
93765
94116
|
status: "ok",
|
|
93766
94117
|
courseId: runtimeCourseId,
|
|
94118
|
+
courseKind: integration.kind,
|
|
93767
94119
|
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
93768
94120
|
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
93769
94121
|
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
@@ -93776,6 +94128,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93776
94128
|
return {
|
|
93777
94129
|
status: "ok",
|
|
93778
94130
|
courseId: runtimeCourseId,
|
|
94131
|
+
courseKind: integration.kind,
|
|
93779
94132
|
xpAwarded: result.xpAwarded,
|
|
93780
94133
|
masteredUnits: result.masteredUnitsApplied,
|
|
93781
94134
|
pctCompleteApp: result.pctCompleteApp,
|
|
@@ -93792,7 +94145,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93792
94145
|
const client = this.requireClient();
|
|
93793
94146
|
const db = this.deps.db;
|
|
93794
94147
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
93795
|
-
where:
|
|
94148
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), subject ? eq(gameTimebackIntegrations.subject, subject) : undefined, isActiveBaseGameTimebackIntegration())
|
|
93796
94149
|
});
|
|
93797
94150
|
if (integrations.length === 0) {
|
|
93798
94151
|
throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
|
|
@@ -93952,7 +94305,6 @@ var init_timeback_service = __esm(async () => {
|
|
|
93952
94305
|
}) {
|
|
93953
94306
|
return this.withClientTelemetry(async () => {
|
|
93954
94307
|
const client = this.requireClient();
|
|
93955
|
-
const db = this.deps.db;
|
|
93956
94308
|
const hasWindowStartedAtMs = windowStartedAtMs !== undefined;
|
|
93957
94309
|
const hasWindowSequence = windowSequence !== undefined;
|
|
93958
94310
|
if (hasWindowStartedAtMs === hasWindowSequence) {
|
|
@@ -93980,17 +94332,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
93980
94332
|
return inFlightHeartbeat;
|
|
93981
94333
|
}
|
|
93982
94334
|
const pendingHeartbeat = (async () => {
|
|
93983
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
93984
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
93985
|
-
});
|
|
93986
|
-
if (!integration) {
|
|
93987
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
93988
|
-
}
|
|
93989
94335
|
const {
|
|
93990
94336
|
courseId: runtimeCourseId,
|
|
93991
94337
|
courseName: runtimeCourseName,
|
|
94338
|
+
extensions: runtimeExtensions,
|
|
93992
94339
|
attributes: courseAttributes
|
|
93993
|
-
} =
|
|
94340
|
+
} = await this.resolveRuntimeCourse({
|
|
94341
|
+
gameId,
|
|
94342
|
+
studentId,
|
|
94343
|
+
activityData,
|
|
94344
|
+
extensions: TimebackService.addResumeIdToExtensions(undefined, effectiveResumeId)
|
|
94345
|
+
});
|
|
93994
94346
|
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
93995
94347
|
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
93996
94348
|
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
@@ -94006,7 +94358,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
94006
94358
|
courseId: runtimeCourseId,
|
|
94007
94359
|
courseName: runtimeCourseName,
|
|
94008
94360
|
studentEmail: activityData.studentEmail,
|
|
94009
|
-
extensions:
|
|
94361
|
+
extensions: runtimeExtensions,
|
|
94010
94362
|
...runId ? { runId } : {}
|
|
94011
94363
|
});
|
|
94012
94364
|
}
|
|
@@ -94111,7 +94463,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
94111
94463
|
const db = this.deps.db;
|
|
94112
94464
|
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
94113
94465
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
94114
|
-
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject),
|
|
94466
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveBaseGameTimebackIntegration())
|
|
94115
94467
|
});
|
|
94116
94468
|
if (!integration) {
|
|
94117
94469
|
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
@@ -94980,9 +95332,47 @@ var init_account_service = __esm(async () => {
|
|
|
94980
95332
|
await init_errors8();
|
|
94981
95333
|
});
|
|
94982
95334
|
|
|
95335
|
+
// ../api-core/src/services/embed-partner.service.ts
|
|
95336
|
+
class EmbedPartnerService {
|
|
95337
|
+
deps;
|
|
95338
|
+
constructor(deps) {
|
|
95339
|
+
this.deps = deps;
|
|
95340
|
+
}
|
|
95341
|
+
async ensurePartner(clientId, name2) {
|
|
95342
|
+
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
|
|
95343
|
+
target: embedPartners.clientId,
|
|
95344
|
+
set: { name: name2, updatedAt: new Date }
|
|
95345
|
+
}).returning();
|
|
95346
|
+
if (!partner) {
|
|
95347
|
+
throw new InternalError("Failed to ensure embed partner");
|
|
95348
|
+
}
|
|
95349
|
+
return partner;
|
|
95350
|
+
}
|
|
95351
|
+
addOrigin(clientId, origin) {
|
|
95352
|
+
return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
|
|
95353
|
+
THEN ${embedPartners.allowedOrigins}
|
|
95354
|
+
ELSE array_append(${embedPartners.allowedOrigins}, ${origin}) END`);
|
|
95355
|
+
}
|
|
95356
|
+
removeOrigin(clientId, origin) {
|
|
95357
|
+
return this.setOrigins(clientId, sql`array_remove(${embedPartners.allowedOrigins}, ${origin})`);
|
|
95358
|
+
}
|
|
95359
|
+
async setOrigins(clientId, origins) {
|
|
95360
|
+
const [partner] = await this.deps.db.update(embedPartners).set({ allowedOrigins: origins, updatedAt: new Date }).where(eq(embedPartners.clientId, clientId)).returning();
|
|
95361
|
+
if (!partner) {
|
|
95362
|
+
throw new NotFoundError("Embed partner", clientId);
|
|
95363
|
+
}
|
|
95364
|
+
return partner;
|
|
95365
|
+
}
|
|
95366
|
+
}
|
|
95367
|
+
var init_embed_partner_service = __esm(() => {
|
|
95368
|
+
init_drizzle_orm();
|
|
95369
|
+
init_tables_index();
|
|
95370
|
+
init_errors2();
|
|
95371
|
+
});
|
|
95372
|
+
|
|
94983
95373
|
// ../api-core/src/utils/embed.util.ts
|
|
94984
|
-
function canPartnerLaunch(partner, origin
|
|
94985
|
-
return partner.enabled && partner.allowedOrigins.includes(origin)
|
|
95374
|
+
function canPartnerLaunch(partner, origin) {
|
|
95375
|
+
return partner.enabled && partner.allowedOrigins.includes(origin);
|
|
94986
95376
|
}
|
|
94987
95377
|
function embedPageUrl(hubOrigin, slug, parentOrigin) {
|
|
94988
95378
|
const url = new URL(`${EMBED_PAGE_PATH}/${encodeURIComponent(slug)}`, hubOrigin);
|
|
@@ -95010,12 +95400,9 @@ class EmbedService {
|
|
|
95010
95400
|
return partner;
|
|
95011
95401
|
}
|
|
95012
95402
|
async isFrameOriginAllowed(origin, slug) {
|
|
95013
|
-
const
|
|
95014
|
-
|
|
95015
|
-
|
|
95016
|
-
gameId: embedPartnerGames.gameId
|
|
95017
|
-
}).from(embedPartnerGames).innerJoin(embedPartners, eq(embedPartners.id, embedPartnerGames.partnerId)).innerJoin(games, eq(games.id, embedPartnerGames.gameId)).where(eq(games.slug, slug));
|
|
95018
|
-
return grants.some((grant) => canPartnerLaunch({ ...grant, games: [grant] }, origin, grant.gameId));
|
|
95403
|
+
const game = this.deps.db.select({ id: games.id }).from(games).where(eq(games.slug, slug));
|
|
95404
|
+
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);
|
|
95405
|
+
return partner !== undefined;
|
|
95019
95406
|
}
|
|
95020
95407
|
async createLaunch(partner, input) {
|
|
95021
95408
|
const game = await this.deps.db.query.games.findFirst({
|
|
@@ -95026,9 +95413,8 @@ class EmbedService {
|
|
|
95026
95413
|
throw new NotFoundError("Game", input.gameSlug);
|
|
95027
95414
|
}
|
|
95028
95415
|
setAttributes({ "app.game.id": game.id, "app.game.slug": game.slug });
|
|
95029
|
-
|
|
95030
|
-
|
|
95031
|
-
throw new AccessDeniedError("This partner may not launch that game into that origin");
|
|
95416
|
+
if (!canPartnerLaunch(partner, input.parentOrigin)) {
|
|
95417
|
+
throw new AccessDeniedError("This partner may not launch into that origin");
|
|
95032
95418
|
}
|
|
95033
95419
|
setAttributes({ "app.user.timeback_id": input.timebackId });
|
|
95034
95420
|
const user = await this.deps.resolveTimebackStudent(input.timebackId);
|
|
@@ -95090,20 +95476,13 @@ class EmbedService {
|
|
|
95090
95476
|
throw new NotFoundError("Embed launch", launchId);
|
|
95091
95477
|
}
|
|
95092
95478
|
setAttributes({ "app.game.id": launch.gameId, "app.user.id": launch.userId });
|
|
95093
|
-
const
|
|
95094
|
-
const outcome = renewalRefusal(launch, () => canPartnerLaunch({ ...partner, games: grants }, launch.parentOrigin, launch.gameId));
|
|
95479
|
+
const outcome = renewalRefusal(launch, () => canPartnerLaunch(partner, launch.parentOrigin));
|
|
95095
95480
|
setAttributes({ "app.embed.outcome": outcome ?? "renewed" });
|
|
95096
95481
|
if (outcome) {
|
|
95097
95482
|
throw new AccessDeniedError("This launch has ended");
|
|
95098
95483
|
}
|
|
95099
95484
|
return this.deps.mintGameToken(launch.gameId, launch.userId);
|
|
95100
95485
|
}
|
|
95101
|
-
grantsFor(partnerId, gameId) {
|
|
95102
|
-
return this.deps.db.query.embedPartnerGames.findMany({
|
|
95103
|
-
where: and(eq(embedPartnerGames.partnerId, partnerId), eq(embedPartnerGames.gameId, gameId)),
|
|
95104
|
-
columns: { gameId: true }
|
|
95105
|
-
});
|
|
95106
|
-
}
|
|
95107
95486
|
async endLaunch(launchId) {
|
|
95108
95487
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
95109
95488
|
}
|
|
@@ -95120,15 +95499,12 @@ class EmbedService {
|
|
|
95120
95499
|
"app.embed.partner_id": launch.partnerId,
|
|
95121
95500
|
"app.user.id": launch.userId
|
|
95122
95501
|
});
|
|
95123
|
-
const
|
|
95124
|
-
|
|
95125
|
-
|
|
95126
|
-
|
|
95127
|
-
}),
|
|
95128
|
-
this.grantsFor(launch.partnerId, launch.gameId)
|
|
95129
|
-
]);
|
|
95502
|
+
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
95503
|
+
where: eq(embedPartners.id, launch.partnerId),
|
|
95504
|
+
columns: { enabled: true, allowedOrigins: true }
|
|
95505
|
+
});
|
|
95130
95506
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
95131
|
-
const stillAllowed = partner !== undefined && canPartnerLaunch(
|
|
95507
|
+
const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
|
|
95132
95508
|
if (!originMatches || !stillAllowed) {
|
|
95133
95509
|
await this.endLaunch(launch.id);
|
|
95134
95510
|
setAttributes({
|
|
@@ -95546,14 +95922,16 @@ function createStandaloneServices(deps) {
|
|
|
95546
95922
|
return { ...profile, timeback: timebackData };
|
|
95547
95923
|
}
|
|
95548
95924
|
});
|
|
95925
|
+
const embedPartner = new EmbedPartnerService({ db });
|
|
95549
95926
|
const verify = new VerifyService({
|
|
95550
95927
|
db,
|
|
95551
95928
|
validateGameToken: auth.validateGameToken.bind(auth)
|
|
95552
95929
|
});
|
|
95553
|
-
return { account, embed, lti, session, user, verify };
|
|
95930
|
+
return { account, embed, embedPartner, lti, session, user, verify };
|
|
95554
95931
|
}
|
|
95555
95932
|
var init_standalone = __esm(async () => {
|
|
95556
95933
|
init_spans();
|
|
95934
|
+
init_embed_partner_service();
|
|
95557
95935
|
init_embed_service();
|
|
95558
95936
|
init_lti_service();
|
|
95559
95937
|
init_session_service();
|
|
@@ -111998,7 +112376,7 @@ function ilike2(column6, value) {
|
|
|
111998
112376
|
function notIlike2(column6, value) {
|
|
111999
112377
|
return sql4`${column6} not ilike ${value}`;
|
|
112000
112378
|
}
|
|
112001
|
-
function
|
|
112379
|
+
function arrayContains2(column6, values) {
|
|
112002
112380
|
if (Array.isArray(values)) {
|
|
112003
112381
|
if (values.length === 0) {
|
|
112004
112382
|
throw new Error("arrayContains requires at least one value");
|
|
@@ -135317,7 +135695,7 @@ params: ${params}`);
|
|
|
135317
135695
|
and: () => and2,
|
|
135318
135696
|
applyMixins: () => applyMixins2,
|
|
135319
135697
|
arrayContained: () => arrayContained,
|
|
135320
|
-
arrayContains: () =>
|
|
135698
|
+
arrayContains: () => arrayContains2,
|
|
135321
135699
|
arrayOverlaps: () => arrayOverlaps,
|
|
135322
135700
|
asc: () => asc2,
|
|
135323
135701
|
avg: () => avg,
|
|
@@ -151887,15 +152265,36 @@ function requireUuidParam(value, name2) {
|
|
|
151887
152265
|
}
|
|
151888
152266
|
return value;
|
|
151889
152267
|
}
|
|
152268
|
+
function bearerToken(request) {
|
|
152269
|
+
const header = request.headers.get("Authorization");
|
|
152270
|
+
return header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
152271
|
+
}
|
|
151890
152272
|
async function requirePartner(ctx) {
|
|
151891
|
-
const
|
|
151892
|
-
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
152273
|
+
const token = bearerToken(ctx.request);
|
|
151893
152274
|
const verified = token ? await ctx.providers.auth.verifyEmbedPartnerToken(token) : null;
|
|
151894
152275
|
if (!verified) {
|
|
151895
152276
|
throw ApiError.unauthorized("A valid embed partner access token is required");
|
|
151896
152277
|
}
|
|
151897
152278
|
return ctx.services.embed.getPartnerByClientId(verified.clientId);
|
|
151898
152279
|
}
|
|
152280
|
+
async function requireEmbedAdmin(ctx) {
|
|
152281
|
+
const expected = ctx.config.embedAdminToken;
|
|
152282
|
+
if (!expected) {
|
|
152283
|
+
throw ApiError.serviceUnavailable("Embed partner management is not configured");
|
|
152284
|
+
}
|
|
152285
|
+
const token = bearerToken(ctx.request);
|
|
152286
|
+
const [given, wanted] = token ? await Promise.all([sha256Hex(token), sha256Hex(expected)]) : [];
|
|
152287
|
+
if (!given || given !== wanted) {
|
|
152288
|
+
throw ApiError.unauthorized("A valid embed admin token is required");
|
|
152289
|
+
}
|
|
152290
|
+
}
|
|
152291
|
+
function requireClientIdParam(ctx) {
|
|
152292
|
+
const clientId = EmbedPartnerClientIdSchema.safeParse(ctx.params.clientId);
|
|
152293
|
+
if (!clientId.success) {
|
|
152294
|
+
throw ApiError.badRequest("Missing or invalid clientId");
|
|
152295
|
+
}
|
|
152296
|
+
return clientId.data;
|
|
152297
|
+
}
|
|
151899
152298
|
async function createLaunch(ctx) {
|
|
151900
152299
|
const partner = await requirePartner(ctx);
|
|
151901
152300
|
const body2 = await parseRequestBody(ctx.request, CreateEmbedLaunchSchema);
|
|
@@ -151925,6 +152324,27 @@ async function redeemPass(ctx) {
|
|
|
151925
152324
|
timeback: launch.profile.timeback
|
|
151926
152325
|
};
|
|
151927
152326
|
}
|
|
152327
|
+
async function ensurePartner(ctx) {
|
|
152328
|
+
await requireEmbedAdmin(ctx);
|
|
152329
|
+
const clientId = requireClientIdParam(ctx);
|
|
152330
|
+
const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
|
|
152331
|
+
return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
|
|
152332
|
+
}
|
|
152333
|
+
async function addPartnerOrigin(ctx) {
|
|
152334
|
+
await requireEmbedAdmin(ctx);
|
|
152335
|
+
const clientId = requireClientIdParam(ctx);
|
|
152336
|
+
const body2 = await parseRequestBody(ctx.request, AddEmbedPartnerOriginSchema);
|
|
152337
|
+
return ctx.services.embedPartner.addOrigin(clientId, body2.origin);
|
|
152338
|
+
}
|
|
152339
|
+
async function removePartnerOrigin(ctx) {
|
|
152340
|
+
await requireEmbedAdmin(ctx);
|
|
152341
|
+
const clientId = requireClientIdParam(ctx);
|
|
152342
|
+
const origin = EmbedOriginSchema.safeParse(ctx.url.searchParams.get("origin"));
|
|
152343
|
+
if (!origin.success) {
|
|
152344
|
+
throw ApiError.badRequest("Missing or invalid origin");
|
|
152345
|
+
}
|
|
152346
|
+
return ctx.services.embedPartner.removeOrigin(clientId, origin.data);
|
|
152347
|
+
}
|
|
151928
152348
|
var embed2;
|
|
151929
152349
|
var init_embed_controller = __esm(() => {
|
|
151930
152350
|
init_src();
|
|
@@ -151934,8 +152354,11 @@ var init_embed_controller = __esm(() => {
|
|
|
151934
152354
|
init_utils7();
|
|
151935
152355
|
init_embed_util();
|
|
151936
152356
|
embed2 = defineControllerNames("embed", {
|
|
152357
|
+
addPartnerOrigin,
|
|
151937
152358
|
createLaunch,
|
|
152359
|
+
ensurePartner,
|
|
151938
152360
|
redeemPass,
|
|
152361
|
+
removePartnerOrigin,
|
|
151939
152362
|
renewToken
|
|
151940
152363
|
});
|
|
151941
152364
|
});
|
|
@@ -152542,7 +152965,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
152542
152965
|
limit
|
|
152543
152966
|
};
|
|
152544
152967
|
}
|
|
152545
|
-
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;
|
|
152968
|
+
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;
|
|
152546
152969
|
var init_timeback_controller = __esm(() => {
|
|
152547
152970
|
init_esm();
|
|
152548
152971
|
init_src();
|
|
@@ -152621,6 +153044,27 @@ var init_timeback_controller = __esm(() => {
|
|
|
152621
153044
|
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
|
|
152622
153045
|
return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
|
|
152623
153046
|
});
|
|
153047
|
+
getHoleFillingCourses = requireGameManagementAccess(async (ctx) => {
|
|
153048
|
+
const gameId = ctx.params.gameId;
|
|
153049
|
+
if (!gameId) {
|
|
153050
|
+
throw ApiError.badRequest("Missing gameId");
|
|
153051
|
+
}
|
|
153052
|
+
if (!isValidUUID(gameId)) {
|
|
153053
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
153054
|
+
}
|
|
153055
|
+
return ctx.services.timeback.getHoleFillingCourses(gameId, ctx.user);
|
|
153056
|
+
});
|
|
153057
|
+
createHoleFillingCourse = requireDeveloper(async (ctx) => {
|
|
153058
|
+
const gameId = ctx.params.gameId;
|
|
153059
|
+
if (!gameId) {
|
|
153060
|
+
throw ApiError.badRequest("Missing gameId");
|
|
153061
|
+
}
|
|
153062
|
+
if (!isValidUUID(gameId)) {
|
|
153063
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
153064
|
+
}
|
|
153065
|
+
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackHoleFillingCourseRequestSchema);
|
|
153066
|
+
return ctx.services.timeback.createHoleFillingCourse(gameId, ctx.user, body2);
|
|
153067
|
+
});
|
|
152624
153068
|
updateIntegration = requireDeveloper(async (ctx) => {
|
|
152625
153069
|
const { gameId, courseId } = ctx.params;
|
|
152626
153070
|
if (!gameId || !courseId) {
|
|
@@ -153405,6 +153849,8 @@ var init_timeback_controller = __esm(() => {
|
|
|
153405
153849
|
getIntegrations,
|
|
153406
153850
|
getRemovedIntegrations,
|
|
153407
153851
|
createIntegration,
|
|
153852
|
+
getHoleFillingCourses,
|
|
153853
|
+
createHoleFillingCourse,
|
|
153408
153854
|
updateIntegration,
|
|
153409
153855
|
deactivateCourse,
|
|
153410
153856
|
reactivateCourse,
|