@playcademy/sandbox 0.12.2-beta.1 → 0.12.2-beta.2
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 +511 -155
- package/dist/server.js +511 -155
- 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.2",
|
|
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,16 @@ 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
|
+
}
|
|
21052
21071
|
function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
|
|
21053
21072
|
return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
|
|
21054
21073
|
}
|
|
21055
|
-
|
|
21074
|
+
function isActiveBaseGameTimebackIntegration() {
|
|
21075
|
+
return sql`(${isActiveGameTimebackIntegrationStatus()} AND ${isBaseGameTimebackIntegration()})`;
|
|
21076
|
+
}
|
|
21077
|
+
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
21078
|
var init_helpers2 = __esm(() => {
|
|
21057
21079
|
init_drizzle_orm();
|
|
21058
21080
|
init_table7();
|
|
@@ -37629,12 +37651,12 @@ function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
|
|
|
37629
37651
|
function filterEnrollmentsByGame(enrollments, gameId) {
|
|
37630
37652
|
return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _, ...enrollment }) => enrollment);
|
|
37631
37653
|
}
|
|
37632
|
-
function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
37654
|
+
function mapEnrollmentsToUserEnrollments(enrollments, integrations, baseCourseIdByIntegrationId = new Map) {
|
|
37633
37655
|
const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
|
|
37634
37656
|
const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
|
|
37635
37657
|
return integrations.map((integration) => {
|
|
37636
37658
|
const enrollment = enrollmentByCourse.get(integration.courseId);
|
|
37637
|
-
|
|
37659
|
+
const userEnrollment = {
|
|
37638
37660
|
gameId: integration.gameId,
|
|
37639
37661
|
grade: integration.grade,
|
|
37640
37662
|
subject: integration.subject,
|
|
@@ -37642,6 +37664,17 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
37642
37664
|
orgId: courseToSchool.get(integration.courseId),
|
|
37643
37665
|
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
37644
37666
|
};
|
|
37667
|
+
if (integration.kind !== HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
37668
|
+
return userEnrollment;
|
|
37669
|
+
}
|
|
37670
|
+
const baseCourseId = integration.baseIntegrationId ? baseCourseIdByIntegrationId.get(integration.baseIntegrationId) : undefined;
|
|
37671
|
+
return {
|
|
37672
|
+
...userEnrollment,
|
|
37673
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
37674
|
+
...baseCourseId && { baseCourseId },
|
|
37675
|
+
...enrollment?.beginDate !== undefined && { beginDate: enrollment.beginDate },
|
|
37676
|
+
...enrollment?.metadata && { enrollmentMetadata: enrollment.metadata }
|
|
37677
|
+
};
|
|
37645
37678
|
});
|
|
37646
37679
|
}
|
|
37647
37680
|
function buildGameTimebackSummaries(integrations) {
|
|
@@ -38124,7 +38157,7 @@ var init_game_service = __esm(() => {
|
|
|
38124
38157
|
const db = this.deps.db;
|
|
38125
38158
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
38126
38159
|
columns: { gameId: true, subject: true },
|
|
38127
|
-
where:
|
|
38160
|
+
where: isActiveBaseGameTimebackIntegration(),
|
|
38128
38161
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
38129
38162
|
});
|
|
38130
38163
|
const subjectMap = {};
|
|
@@ -38148,9 +38181,7 @@ var init_game_service = __esm(() => {
|
|
|
38148
38181
|
}
|
|
38149
38182
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
38150
38183
|
columns: { gameId: true, subject: true, status: true },
|
|
38151
|
-
...accessibleGameIds
|
|
38152
|
-
where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
|
|
38153
|
-
},
|
|
38184
|
+
where: and(accessibleGameIds ? inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds]) : undefined, isBaseGameTimebackIntegration()),
|
|
38154
38185
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
38155
38186
|
});
|
|
38156
38187
|
return buildGameTimebackSummaries(integrations);
|
|
@@ -48581,7 +48612,7 @@ function rejectSystemManagedReview(input, context) {
|
|
|
48581
48612
|
});
|
|
48582
48613
|
}
|
|
48583
48614
|
}
|
|
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;
|
|
48615
|
+
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
48616
|
var init_schemas4 = __esm(() => {
|
|
48586
48617
|
init_esm();
|
|
48587
48618
|
init_src();
|
|
@@ -48617,6 +48648,26 @@ var init_schemas4 = __esm(() => {
|
|
|
48617
48648
|
masterableUnits: numberType().int().nonnegative(),
|
|
48618
48649
|
level: stringType().trim().min(1).optional()
|
|
48619
48650
|
});
|
|
48651
|
+
RESERVED_TIMEBACK_COURSE_SOURCED_IDS = new Set([
|
|
48652
|
+
"hole-filling",
|
|
48653
|
+
"removed",
|
|
48654
|
+
"components",
|
|
48655
|
+
"component-resources",
|
|
48656
|
+
"structure"
|
|
48657
|
+
]);
|
|
48658
|
+
TimebackCourseSourcedIdSchema = stringType().trim().min(3).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
|
|
48659
|
+
message: "Use letters, numbers, dots, dashes, or underscores, starting with a letter or number"
|
|
48660
|
+
}).refine((value) => !RESERVED_TIMEBACK_COURSE_SOURCED_IDS.has(value.toLowerCase()), {
|
|
48661
|
+
message: "This course ID is reserved"
|
|
48662
|
+
});
|
|
48663
|
+
CreateGameTimebackHoleFillingCourseRequestSchema = objectType({
|
|
48664
|
+
baseCourseId: stringType().trim().min(1),
|
|
48665
|
+
title: stringType().trim().min(1),
|
|
48666
|
+
courseCode: stringType().trim().min(1),
|
|
48667
|
+
totalXp: numberType().int().positive(),
|
|
48668
|
+
masterableUnits: numberType().int().nonnegative(),
|
|
48669
|
+
courseId: TimebackCourseSourcedIdSchema.optional()
|
|
48670
|
+
});
|
|
48620
48671
|
TimebackActivityDataSchema = objectType({
|
|
48621
48672
|
activityId: stringType().min(1),
|
|
48622
48673
|
activityName: stringType().optional(),
|
|
@@ -48948,21 +48999,20 @@ function isCanonicalOrigin(value) {
|
|
|
48948
48999
|
}
|
|
48949
49000
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
48950
49001
|
}
|
|
48951
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema,
|
|
49002
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
48952
49003
|
var init_schemas5 = __esm(() => {
|
|
48953
49004
|
init_esm();
|
|
48954
49005
|
LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
|
|
48955
49006
|
EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
|
|
48956
49007
|
message: "Origin must be a bare https origin like https://example.com, with no path or slash"
|
|
48957
49008
|
});
|
|
48958
|
-
|
|
48959
|
-
|
|
48960
|
-
|
|
48961
|
-
|
|
48962
|
-
|
|
48963
|
-
|
|
49009
|
+
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
49010
|
+
EnsureEmbedPartnerSchema = objectType({
|
|
49011
|
+
name: stringType().trim().min(1).max(100)
|
|
49012
|
+
}).strict();
|
|
49013
|
+
AddEmbedPartnerOriginSchema = objectType({
|
|
49014
|
+
origin: EmbedOriginSchema
|
|
48964
49015
|
}).strict();
|
|
48965
|
-
UpdateEmbedPartnerSchema = CreateEmbedPartnerSchema.omit({ clientId: true }).partial();
|
|
48966
49016
|
CreateEmbedLaunchSchema = objectType({
|
|
48967
49017
|
timebackId: stringType().trim().min(1).max(200),
|
|
48968
49018
|
gameSlug: stringType().trim().min(1).max(100),
|
|
@@ -76120,6 +76170,7 @@ class CourseAssessments {
|
|
|
76120
76170
|
}
|
|
76121
76171
|
function buildCoursePayload(config) {
|
|
76122
76172
|
return {
|
|
76173
|
+
...config.course.sourcedId && { sourcedId: config.course.sourcedId },
|
|
76123
76174
|
status: "active",
|
|
76124
76175
|
title: config.course.title,
|
|
76125
76176
|
courseCode: config.course.courseCode,
|
|
@@ -76166,6 +76217,16 @@ function buildComponentResourcePayload(config, sourcedIds) {
|
|
|
76166
76217
|
lessonType: config.componentResource.lessonType
|
|
76167
76218
|
};
|
|
76168
76219
|
}
|
|
76220
|
+
async function findOrUndefined(request) {
|
|
76221
|
+
try {
|
|
76222
|
+
return await request;
|
|
76223
|
+
} catch (error) {
|
|
76224
|
+
if (isApiError(error) && error.statusCode === 404) {
|
|
76225
|
+
return;
|
|
76226
|
+
}
|
|
76227
|
+
throw error;
|
|
76228
|
+
}
|
|
76229
|
+
}
|
|
76169
76230
|
|
|
76170
76231
|
class CourseIntegration {
|
|
76171
76232
|
core;
|
|
@@ -76204,6 +76265,32 @@ class CourseIntegration {
|
|
|
76204
76265
|
...verboseData && { verboseData }
|
|
76205
76266
|
};
|
|
76206
76267
|
}
|
|
76268
|
+
async findCourse(courseId) {
|
|
76269
|
+
return findOrUndefined(this.core.api.oneroster.courses.get(courseId));
|
|
76270
|
+
}
|
|
76271
|
+
async resumeSetup(courseId, config) {
|
|
76272
|
+
const oneroster = this.core.api.oneroster;
|
|
76273
|
+
const sourcedIds = deriveSourcedIds(courseId);
|
|
76274
|
+
if (!await findOrUndefined(oneroster.courses.getComponent(sourcedIds.component))) {
|
|
76275
|
+
await oneroster.courses.createComponent(buildComponentPayload(config, sourcedIds));
|
|
76276
|
+
addEvent("timeback.component_created", {
|
|
76277
|
+
"app.timeback.component_id": sourcedIds.component
|
|
76278
|
+
});
|
|
76279
|
+
}
|
|
76280
|
+
if (!await findOrUndefined(oneroster.resources.get(sourcedIds.resource))) {
|
|
76281
|
+
await oneroster.resources.create(buildResourcePayload(config, sourcedIds.resource));
|
|
76282
|
+
addEvent("timeback.resource_created", {
|
|
76283
|
+
"app.timeback.resource_id": sourcedIds.resource
|
|
76284
|
+
});
|
|
76285
|
+
}
|
|
76286
|
+
if (!await findOrUndefined(oneroster.courses.getComponentResource(sourcedIds.componentResource))) {
|
|
76287
|
+
await oneroster.courses.createComponentResource(buildComponentResourcePayload(config, sourcedIds));
|
|
76288
|
+
addEvent("timeback.component_resource_link_created", {
|
|
76289
|
+
"app.timeback.component_resource_id": sourcedIds.componentResource
|
|
76290
|
+
});
|
|
76291
|
+
}
|
|
76292
|
+
await this.update(courseId, config);
|
|
76293
|
+
}
|
|
76207
76294
|
async update(courseId, config) {
|
|
76208
76295
|
const oneroster = this.core.api.oneroster;
|
|
76209
76296
|
const sourcedIds = deriveSourcedIds(courseId);
|
|
@@ -76343,6 +76430,8 @@ function createCourseNamespace(core) {
|
|
|
76343
76430
|
update: (courseId, config) => integration.update(courseId, config),
|
|
76344
76431
|
verify: (courseId) => integration.verify(courseId),
|
|
76345
76432
|
getConfig: (courseId) => integration.getConfig(courseId),
|
|
76433
|
+
findCourse: (courseId) => integration.findCourse(courseId),
|
|
76434
|
+
resumeSetup: (courseId, config) => integration.resumeSetup(courseId, config),
|
|
76346
76435
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
76347
76436
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
76348
76437
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
@@ -76427,7 +76516,9 @@ class RosterEnrollments {
|
|
|
76427
76516
|
status: "active",
|
|
76428
76517
|
grades,
|
|
76429
76518
|
subjects,
|
|
76430
|
-
school: enrollment.school
|
|
76519
|
+
school: enrollment.school,
|
|
76520
|
+
beginDate: enrollment.beginDate,
|
|
76521
|
+
...enrollment.metadata && { metadata: enrollment.metadata }
|
|
76431
76522
|
};
|
|
76432
76523
|
});
|
|
76433
76524
|
this.core.caches.enrollments.set(studentId, enrollments);
|
|
@@ -87231,7 +87322,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
87231
87322
|
}) {
|
|
87232
87323
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
87233
87324
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
87234
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
87325
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
87235
87326
|
});
|
|
87236
87327
|
const matchingIntegrations = new Map;
|
|
87237
87328
|
for (const integration of integrations) {
|
|
@@ -87620,7 +87711,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
87620
87711
|
}) {
|
|
87621
87712
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
87622
87713
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
87623
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
87714
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
87624
87715
|
});
|
|
87625
87716
|
const orderedIntegrations = integrations.toSorted((left, right) => {
|
|
87626
87717
|
const subjectDifference = left.subject.localeCompare(right.subject);
|
|
@@ -87699,7 +87790,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
87699
87790
|
async candidateIntegrations(gameId, studentId, input, db = this.deps.db) {
|
|
87700
87791
|
const filters = [
|
|
87701
87792
|
eq(gameTimebackIntegrations.gameId, gameId),
|
|
87702
|
-
|
|
87793
|
+
isActiveBaseGameTimebackIntegration()
|
|
87703
87794
|
];
|
|
87704
87795
|
if (input.subject) {
|
|
87705
87796
|
filters.push(eq(gameTimebackIntegrations.subject, input.subject));
|
|
@@ -90641,6 +90732,50 @@ function timebackConfigMatchesCreateIntegrationRequest(config, request) {
|
|
|
90641
90732
|
const requestedLevel = request.level ?? deriveTimebackCourseLevelFromGrade(request.grade);
|
|
90642
90733
|
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
90734
|
}
|
|
90735
|
+
function buildHoleFillingCourseConfig(baseConfig, request) {
|
|
90736
|
+
const baseCourseMetadata = isRecord4(baseConfig.course.metadata) ? baseConfig.course.metadata : {};
|
|
90737
|
+
const baseMetrics = isRecord4(baseCourseMetadata.metrics) ? baseCourseMetadata.metrics : {};
|
|
90738
|
+
const baseResourceMetadata = isRecord4(baseConfig.resource.metadata) ? baseConfig.resource.metadata : {};
|
|
90739
|
+
const basePlaycademy = isPlaycademyResourceMetadata(baseResourceMetadata.playcademy) ? baseResourceMetadata.playcademy : undefined;
|
|
90740
|
+
return {
|
|
90741
|
+
organization: baseConfig.organization,
|
|
90742
|
+
course: {
|
|
90743
|
+
...baseConfig.course,
|
|
90744
|
+
...request.courseId && { sourcedId: request.courseId },
|
|
90745
|
+
title: request.title,
|
|
90746
|
+
courseCode: request.courseCode,
|
|
90747
|
+
metadata: {
|
|
90748
|
+
...baseCourseMetadata,
|
|
90749
|
+
courseType: HOLE_FILLING_COURSE_TYPE,
|
|
90750
|
+
baseCourseSourcedId: request.baseCourseId,
|
|
90751
|
+
metrics: {
|
|
90752
|
+
...baseMetrics,
|
|
90753
|
+
totalXp: request.totalXp,
|
|
90754
|
+
totalLessons: request.masterableUnits,
|
|
90755
|
+
totalGrades: 1
|
|
90756
|
+
}
|
|
90757
|
+
}
|
|
90758
|
+
},
|
|
90759
|
+
component: { ...baseConfig.component, title: `${request.title} Activities` },
|
|
90760
|
+
resource: {
|
|
90761
|
+
...baseConfig.resource,
|
|
90762
|
+
title: `${request.title} Game`,
|
|
90763
|
+
metadata: {
|
|
90764
|
+
...baseResourceMetadata,
|
|
90765
|
+
xp: request.totalXp,
|
|
90766
|
+
playcademy: {
|
|
90767
|
+
...basePlaycademy,
|
|
90768
|
+
mastery: {
|
|
90769
|
+
...basePlaycademy?.mastery,
|
|
90770
|
+
masterableUnits: request.masterableUnits
|
|
90771
|
+
}
|
|
90772
|
+
}
|
|
90773
|
+
}
|
|
90774
|
+
},
|
|
90775
|
+
componentResource: { ...baseConfig.componentResource, title: `${request.title} Lesson` }
|
|
90776
|
+
};
|
|
90777
|
+
}
|
|
90778
|
+
var HOLE_FILLING_COURSE_TYPE = "hole-filling";
|
|
90644
90779
|
var init_timeback_create_integration_util = __esm(() => {
|
|
90645
90780
|
init_constants2();
|
|
90646
90781
|
init_types2();
|
|
@@ -90656,7 +90791,7 @@ async function promoteCompletedCourse({
|
|
|
90656
90791
|
enrollments: prefetchedEnrollments
|
|
90657
90792
|
}) {
|
|
90658
90793
|
const subjectIntegrations = await db.query.gameTimebackIntegrations.findMany({
|
|
90659
|
-
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject),
|
|
90794
|
+
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveBaseGameTimebackIntegration())
|
|
90660
90795
|
});
|
|
90661
90796
|
const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
|
|
90662
90797
|
if (!nextIntegration) {
|
|
@@ -90723,6 +90858,8 @@ function toGameTimebackIntegration(integration) {
|
|
|
90723
90858
|
courseId: integration.courseId,
|
|
90724
90859
|
grade: integration.grade,
|
|
90725
90860
|
subject: integration.subject,
|
|
90861
|
+
kind: integration.kind,
|
|
90862
|
+
baseIntegrationId: integration.baseIntegrationId,
|
|
90726
90863
|
totalXp: integration.totalXp ?? null,
|
|
90727
90864
|
status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
|
|
90728
90865
|
deactivatedAt: integration.deactivatedAt ?? null,
|
|
@@ -90760,6 +90897,9 @@ var init_timeback_removed_integration_util = __esm(() => {
|
|
|
90760
90897
|
|
|
90761
90898
|
// ../api-core/src/services/timeback.service.ts
|
|
90762
90899
|
import { createHash as createHash3 } from "node:crypto";
|
|
90900
|
+
function isUnregisteredHoleFillingOf(course, base) {
|
|
90901
|
+
return course.metadata?.courseType === HOLE_FILLING_COURSE_TYPE && course.metadata.baseCourseSourcedId === base.courseId;
|
|
90902
|
+
}
|
|
90763
90903
|
async function findGameTimebackIntegrationForUpdate(db, condition) {
|
|
90764
90904
|
const [integration] = await db.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
|
|
90765
90905
|
return integration;
|
|
@@ -91166,13 +91306,29 @@ var init_timeback_service = __esm(async () => {
|
|
|
91166
91306
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
91167
91307
|
where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
|
|
91168
91308
|
});
|
|
91169
|
-
return mapEnrollmentsToUserEnrollments(enrollments, integrations);
|
|
91309
|
+
return mapEnrollmentsToUserEnrollments(enrollments, integrations, await this.loadBaseCourseIds(integrations));
|
|
91170
91310
|
} catch (error) {
|
|
91171
91311
|
setAttribute("app.timeback.enrollment_fetch", "failed");
|
|
91172
91312
|
setAttribute("app.timeback.enrollment_fetch_error", errorMessage(error));
|
|
91173
91313
|
return [];
|
|
91174
91314
|
}
|
|
91175
91315
|
}
|
|
91316
|
+
async loadBaseCourseIds(integrations) {
|
|
91317
|
+
const courseIdById = new Map(integrations.map((row) => [row.id, row.courseId]));
|
|
91318
|
+
const missingBaseIds = [
|
|
91319
|
+
...new Set(integrations.map((row) => row.baseIntegrationId).filter((id) => id !== null && !courseIdById.has(id)))
|
|
91320
|
+
];
|
|
91321
|
+
if (missingBaseIds.length > 0) {
|
|
91322
|
+
const baseRows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91323
|
+
columns: { id: true, courseId: true },
|
|
91324
|
+
where: inArray(gameTimebackIntegrations.id, missingBaseIds)
|
|
91325
|
+
});
|
|
91326
|
+
for (const row of baseRows) {
|
|
91327
|
+
courseIdById.set(row.id, row.courseId);
|
|
91328
|
+
}
|
|
91329
|
+
}
|
|
91330
|
+
return courseIdById;
|
|
91331
|
+
}
|
|
91176
91332
|
async setupIntegration(gameId, request, user) {
|
|
91177
91333
|
return this.withClientTelemetry(async () => {
|
|
91178
91334
|
const client = this.requireClient();
|
|
@@ -91183,7 +91339,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91183
91339
|
"app.timeback.course_count": courses.length
|
|
91184
91340
|
});
|
|
91185
91341
|
const existing = await db.query.gameTimebackIntegrations.findMany({
|
|
91186
|
-
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
91342
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration())
|
|
91187
91343
|
});
|
|
91188
91344
|
const integrations = [];
|
|
91189
91345
|
const verboseData = [];
|
|
@@ -91331,13 +91487,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
91331
91487
|
"app.timeback.subject": request.subject
|
|
91332
91488
|
});
|
|
91333
91489
|
const activeConflict = await db.query.gameTimebackIntegrations.findFirst({
|
|
91334
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject),
|
|
91490
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveBaseGameTimebackIntegration())
|
|
91335
91491
|
});
|
|
91336
91492
|
if (activeConflict) {
|
|
91337
91493
|
throw new ConflictError(`A TimeBack integration already exists for ${request.subject} Grade ${request.grade}`);
|
|
91338
91494
|
}
|
|
91339
91495
|
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))
|
|
91496
|
+
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
91497
|
});
|
|
91342
91498
|
for (const candidate of removedCandidates) {
|
|
91343
91499
|
const config = await client.course.getConfig(candidate.courseId).catch((error) => {
|
|
@@ -91375,10 +91531,10 @@ var init_timeback_service = __esm(async () => {
|
|
|
91375
91531
|
}
|
|
91376
91532
|
}
|
|
91377
91533
|
const reference = await db.query.gameTimebackIntegrations.findFirst({
|
|
91378
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91534
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration()),
|
|
91379
91535
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
91380
91536
|
}) ?? await db.query.gameTimebackIntegrations.findFirst({
|
|
91381
|
-
where: eq(gameTimebackIntegrations.gameId, gameId),
|
|
91537
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration()),
|
|
91382
91538
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
91383
91539
|
});
|
|
91384
91540
|
if (!reference) {
|
|
@@ -91408,10 +91564,77 @@ var init_timeback_service = __esm(async () => {
|
|
|
91408
91564
|
return integration;
|
|
91409
91565
|
});
|
|
91410
91566
|
}
|
|
91567
|
+
async createHoleFillingCourse(gameId, user, request) {
|
|
91568
|
+
return this.withClientTelemetry(async () => {
|
|
91569
|
+
const client = this.requireClient();
|
|
91570
|
+
const db = this.deps.db;
|
|
91571
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91572
|
+
TimebackService.recordIntegrationOperation("create_hole_filling_course", {
|
|
91573
|
+
"app.timeback.base_course_id": request.baseCourseId,
|
|
91574
|
+
"app.timeback.custom_course_id": Boolean(request.courseId)
|
|
91575
|
+
});
|
|
91576
|
+
const integration = await db.transaction(async (tx) => {
|
|
91577
|
+
const database = tx;
|
|
91578
|
+
const base = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, request.baseCourseId), isActiveBaseGameTimebackIntegration()));
|
|
91579
|
+
if (!base) {
|
|
91580
|
+
throw new NotFoundError("Active base course", `${gameId}:${request.baseCourseId}`);
|
|
91581
|
+
}
|
|
91582
|
+
const config = buildHoleFillingCourseConfig(await client.course.getConfig(base.courseId), request);
|
|
91583
|
+
let courseId = request.courseId;
|
|
91584
|
+
if (courseId) {
|
|
91585
|
+
const takenLocally = await database.query.gameTimebackIntegrations.findFirst({
|
|
91586
|
+
columns: { id: true },
|
|
91587
|
+
where: eq(gameTimebackIntegrations.courseId, courseId)
|
|
91588
|
+
});
|
|
91589
|
+
const remote = takenLocally ? undefined : await client.course.findCourse(courseId);
|
|
91590
|
+
if (takenLocally || remote && !isUnregisteredHoleFillingOf(remote, base)) {
|
|
91591
|
+
throw new ConflictError(`A Timeback course with ID "${courseId}" already exists`);
|
|
91592
|
+
}
|
|
91593
|
+
if (remote) {
|
|
91594
|
+
setAttribute("app.timeback.course_create_mode", "resumed");
|
|
91595
|
+
await client.course.resumeSetup(courseId, config);
|
|
91596
|
+
} else {
|
|
91597
|
+
courseId = undefined;
|
|
91598
|
+
}
|
|
91599
|
+
}
|
|
91600
|
+
if (!courseId) {
|
|
91601
|
+
const created = await client.course.setup(config);
|
|
91602
|
+
courseId = created.courseId;
|
|
91603
|
+
}
|
|
91604
|
+
setAttribute("app.timeback.course_id", courseId);
|
|
91605
|
+
const [inserted] = await database.insert(gameTimebackIntegrations).values({
|
|
91606
|
+
gameId,
|
|
91607
|
+
courseId,
|
|
91608
|
+
grade: base.grade,
|
|
91609
|
+
subject: base.subject,
|
|
91610
|
+
totalXp: request.totalXp,
|
|
91611
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
91612
|
+
baseIntegrationId: base.id
|
|
91613
|
+
}).returning();
|
|
91614
|
+
return inserted;
|
|
91615
|
+
});
|
|
91616
|
+
if (!integration) {
|
|
91617
|
+
throw new InternalError("Hole-filling course creation did not return an integration");
|
|
91618
|
+
}
|
|
91619
|
+
return toGameTimebackIntegration(integration);
|
|
91620
|
+
});
|
|
91621
|
+
}
|
|
91622
|
+
async getHoleFillingCourses(gameId, user) {
|
|
91623
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
91624
|
+
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91625
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.kind, HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND), isActiveGameTimebackIntegrationStatus()),
|
|
91626
|
+
orderBy: (table, { asc }) => [
|
|
91627
|
+
asc(table.subject),
|
|
91628
|
+
asc(table.grade),
|
|
91629
|
+
asc(table.createdAt)
|
|
91630
|
+
]
|
|
91631
|
+
});
|
|
91632
|
+
return rows.map((row) => toGameTimebackIntegration(row));
|
|
91633
|
+
}
|
|
91411
91634
|
async getIntegrations(gameId, user) {
|
|
91412
91635
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
91413
91636
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91414
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91637
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
91415
91638
|
});
|
|
91416
91639
|
return rows.map((row) => toGameTimebackIntegration(row));
|
|
91417
91640
|
}
|
|
@@ -91420,7 +91643,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91420
91643
|
const client = this.requireClient();
|
|
91421
91644
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
91422
91645
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91423
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
|
|
91646
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration()),
|
|
91424
91647
|
orderBy: (table, { asc }) => [asc(table.subject), asc(table.grade)]
|
|
91425
91648
|
});
|
|
91426
91649
|
setAttribute("app.timeback.removed_integration_count", rows.length);
|
|
@@ -91451,6 +91674,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
91451
91674
|
if (!integration) {
|
|
91452
91675
|
throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
|
|
91453
91676
|
}
|
|
91677
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
91678
|
+
throw new ConflictError("This is a hole-filling course and may be referenced by Timeback, so it can't be removed.");
|
|
91679
|
+
}
|
|
91680
|
+
const activeHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
91681
|
+
columns: { id: true },
|
|
91682
|
+
where: and(eq(gameTimebackIntegrations.baseIntegrationId, integration.id), isActiveGameTimebackIntegrationStatus())
|
|
91683
|
+
});
|
|
91684
|
+
if (activeHoleFilling) {
|
|
91685
|
+
setAttribute("app.timeback.removal_blocked", "hole_filling_courses");
|
|
91686
|
+
throw new ConflictError("This course has hole-filling courses that may be referenced by Timeback, so it can't be removed.");
|
|
91687
|
+
}
|
|
91454
91688
|
const activeEnrollments = await client.roster.getCourseRoster(courseId, {
|
|
91455
91689
|
role: "student",
|
|
91456
91690
|
includeUsers: false
|
|
@@ -91488,11 +91722,18 @@ var init_timeback_service = __esm(async () => {
|
|
|
91488
91722
|
if (!integration) {
|
|
91489
91723
|
throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
|
|
91490
91724
|
}
|
|
91491
|
-
|
|
91492
|
-
|
|
91493
|
-
|
|
91494
|
-
|
|
91495
|
-
|
|
91725
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
91726
|
+
const base = integration.baseIntegrationId ? await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.id, integration.baseIntegrationId), isActiveBaseGameTimebackIntegration())) : undefined;
|
|
91727
|
+
if (!base) {
|
|
91728
|
+
throw new ConflictError("Restore the base course first: a hole-filling course can only be active beside its base course.");
|
|
91729
|
+
}
|
|
91730
|
+
} else {
|
|
91731
|
+
const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
91732
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveBaseGameTimebackIntegration())
|
|
91733
|
+
});
|
|
91734
|
+
if (activeConflict) {
|
|
91735
|
+
throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
|
|
91736
|
+
}
|
|
91496
91737
|
}
|
|
91497
91738
|
const now = new Date;
|
|
91498
91739
|
const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
|
|
@@ -91531,52 +91772,66 @@ var init_timeback_service = __esm(async () => {
|
|
|
91531
91772
|
TimebackService.recordIntegrationOperation("update_integration", {
|
|
91532
91773
|
"app.timeback.course_id": courseId
|
|
91533
91774
|
});
|
|
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())
|
|
91775
|
+
const updated = await this.deps.db.transaction(async (tx) => {
|
|
91776
|
+
const database = tx;
|
|
91777
|
+
const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
|
|
91778
|
+
if (!integration) {
|
|
91779
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
91780
|
+
}
|
|
91781
|
+
const timebackConfig = await client.course.getConfig(courseId);
|
|
91782
|
+
const liveSubject = timebackConfig.course.subjects[0];
|
|
91783
|
+
const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
|
|
91784
|
+
if (!isTimebackSubject(subject)) {
|
|
91785
|
+
throw new ValidationError(`Invalid subject "${subject}"`);
|
|
91786
|
+
}
|
|
91787
|
+
setAttributes({
|
|
91788
|
+
"app.timeback.grade": integration.grade,
|
|
91789
|
+
"app.timeback.previous_subject": integration.subject,
|
|
91790
|
+
"app.timeback.next_subject": subject
|
|
91554
91791
|
});
|
|
91555
|
-
if (
|
|
91556
|
-
|
|
91792
|
+
if (subject !== integration.subject) {
|
|
91793
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
91794
|
+
throw new ValidationError("A hole-filling course must keep the subject of its base course");
|
|
91795
|
+
}
|
|
91796
|
+
const linkedHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
91797
|
+
columns: { id: true },
|
|
91798
|
+
where: eq(gameTimebackIntegrations.baseIntegrationId, integration.id)
|
|
91799
|
+
});
|
|
91800
|
+
if (linkedHoleFilling) {
|
|
91801
|
+
throw new ValidationError("This course has hole-filling courses, so its subject cannot change");
|
|
91802
|
+
}
|
|
91557
91803
|
}
|
|
91558
|
-
|
|
91559
|
-
|
|
91560
|
-
|
|
91561
|
-
|
|
91562
|
-
|
|
91563
|
-
|
|
91804
|
+
if (subject !== integration.subject) {
|
|
91805
|
+
const subjectConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
91806
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveBaseGameTimebackIntegration())
|
|
91807
|
+
});
|
|
91808
|
+
if (subjectConflict && subjectConflict.id !== integration.id) {
|
|
91809
|
+
throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
|
|
91810
|
+
}
|
|
91811
|
+
}
|
|
91812
|
+
const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
|
|
91813
|
+
const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
|
|
91814
|
+
setAttributes({
|
|
91815
|
+
"app.timeback.total_xp": totalXp,
|
|
91816
|
+
"app.timeback.masterable_units": masterableUnits
|
|
91817
|
+
});
|
|
91818
|
+
await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
|
|
91819
|
+
...patch,
|
|
91820
|
+
subject,
|
|
91821
|
+
totalXp,
|
|
91822
|
+
masterableUnits,
|
|
91823
|
+
grade: integration.grade
|
|
91824
|
+
}));
|
|
91825
|
+
const [row] = await database.update(gameTimebackIntegrations).set({
|
|
91826
|
+
subject,
|
|
91827
|
+
totalXp,
|
|
91828
|
+
updatedAt: new Date
|
|
91829
|
+
}).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
|
|
91830
|
+
if (!row) {
|
|
91831
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
91832
|
+
}
|
|
91833
|
+
return row;
|
|
91564
91834
|
});
|
|
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
91835
|
return toGameTimebackIntegration(updated);
|
|
91581
91836
|
});
|
|
91582
91837
|
}
|
|
@@ -91587,7 +91842,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91587
91842
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91588
91843
|
TimebackService.recordIntegrationOperation("verify_integration");
|
|
91589
91844
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
91590
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91845
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
91591
91846
|
});
|
|
91592
91847
|
if (integrations.length === 0) {
|
|
91593
91848
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -91625,7 +91880,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91625
91880
|
const client = this.requireClient();
|
|
91626
91881
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91627
91882
|
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
91628
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
91883
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
91629
91884
|
});
|
|
91630
91885
|
if (!integration) {
|
|
91631
91886
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -91639,17 +91894,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
91639
91894
|
const db = this.deps.db;
|
|
91640
91895
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91641
91896
|
TimebackService.recordIntegrationOperation("delete_integrations");
|
|
91642
|
-
|
|
91643
|
-
|
|
91897
|
+
await db.transaction(async (tx) => {
|
|
91898
|
+
const database = tx;
|
|
91899
|
+
await database.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId)).for("update");
|
|
91900
|
+
const integrations = await database.select().from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
91901
|
+
if (integrations.length === 0) {
|
|
91902
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
91903
|
+
}
|
|
91904
|
+
setAttribute("app.timeback.integration_count", integrations.length);
|
|
91905
|
+
const holeFillingCount = integrations.filter((integration) => integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND).length;
|
|
91906
|
+
if (holeFillingCount > 0) {
|
|
91907
|
+
setAttribute("app.timeback.hole_filling_count", holeFillingCount);
|
|
91908
|
+
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.`);
|
|
91909
|
+
}
|
|
91910
|
+
for (const integration of integrations) {
|
|
91911
|
+
await client.course.cleanup(integration.courseId);
|
|
91912
|
+
}
|
|
91913
|
+
await database.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
91644
91914
|
});
|
|
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
91915
|
});
|
|
91654
91916
|
}
|
|
91655
91917
|
static patchCourseMetadata(metadata2, totalXp, options) {
|
|
@@ -91829,7 +92091,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91829
92091
|
});
|
|
91830
92092
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
91831
92093
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
91832
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject),
|
|
92094
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
91833
92095
|
});
|
|
91834
92096
|
if (!integration) {
|
|
91835
92097
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
@@ -91972,7 +92234,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
91972
92234
|
const client = this.requireClient();
|
|
91973
92235
|
const db = this.deps.db;
|
|
91974
92236
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
91975
|
-
where:
|
|
92237
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), subject ? eq(gameTimebackIntegrations.subject, subject) : undefined, isActiveBaseGameTimebackIntegration())
|
|
91976
92238
|
});
|
|
91977
92239
|
if (integrations.length === 0) {
|
|
91978
92240
|
throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
|
|
@@ -92161,7 +92423,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92161
92423
|
}
|
|
92162
92424
|
const pendingHeartbeat = (async () => {
|
|
92163
92425
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
92164
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject),
|
|
92426
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
92165
92427
|
});
|
|
92166
92428
|
if (!integration) {
|
|
92167
92429
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
@@ -92291,7 +92553,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92291
92553
|
const db = this.deps.db;
|
|
92292
92554
|
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
92293
92555
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
92294
|
-
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject),
|
|
92556
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveBaseGameTimebackIntegration())
|
|
92295
92557
|
});
|
|
92296
92558
|
if (!integration) {
|
|
92297
92559
|
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
@@ -93160,9 +93422,47 @@ var init_account_service = __esm(async () => {
|
|
|
93160
93422
|
await init_errors8();
|
|
93161
93423
|
});
|
|
93162
93424
|
|
|
93425
|
+
// ../api-core/src/services/embed-partner.service.ts
|
|
93426
|
+
class EmbedPartnerService {
|
|
93427
|
+
deps;
|
|
93428
|
+
constructor(deps) {
|
|
93429
|
+
this.deps = deps;
|
|
93430
|
+
}
|
|
93431
|
+
async ensurePartner(clientId, name2) {
|
|
93432
|
+
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
|
|
93433
|
+
target: embedPartners.clientId,
|
|
93434
|
+
set: { name: name2, updatedAt: new Date }
|
|
93435
|
+
}).returning();
|
|
93436
|
+
if (!partner) {
|
|
93437
|
+
throw new InternalError("Failed to ensure embed partner");
|
|
93438
|
+
}
|
|
93439
|
+
return partner;
|
|
93440
|
+
}
|
|
93441
|
+
addOrigin(clientId, origin) {
|
|
93442
|
+
return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
|
|
93443
|
+
THEN ${embedPartners.allowedOrigins}
|
|
93444
|
+
ELSE array_append(${embedPartners.allowedOrigins}, ${origin}) END`);
|
|
93445
|
+
}
|
|
93446
|
+
removeOrigin(clientId, origin) {
|
|
93447
|
+
return this.setOrigins(clientId, sql`array_remove(${embedPartners.allowedOrigins}, ${origin})`);
|
|
93448
|
+
}
|
|
93449
|
+
async setOrigins(clientId, origins) {
|
|
93450
|
+
const [partner] = await this.deps.db.update(embedPartners).set({ allowedOrigins: origins, updatedAt: new Date }).where(eq(embedPartners.clientId, clientId)).returning();
|
|
93451
|
+
if (!partner) {
|
|
93452
|
+
throw new NotFoundError("Embed partner", clientId);
|
|
93453
|
+
}
|
|
93454
|
+
return partner;
|
|
93455
|
+
}
|
|
93456
|
+
}
|
|
93457
|
+
var init_embed_partner_service = __esm(() => {
|
|
93458
|
+
init_drizzle_orm();
|
|
93459
|
+
init_tables_index();
|
|
93460
|
+
init_errors2();
|
|
93461
|
+
});
|
|
93462
|
+
|
|
93163
93463
|
// ../api-core/src/utils/embed.util.ts
|
|
93164
|
-
function canPartnerLaunch(partner, origin
|
|
93165
|
-
return partner.enabled && partner.allowedOrigins.includes(origin)
|
|
93464
|
+
function canPartnerLaunch(partner, origin) {
|
|
93465
|
+
return partner.enabled && partner.allowedOrigins.includes(origin);
|
|
93166
93466
|
}
|
|
93167
93467
|
function embedPageUrl(hubOrigin, slug, parentOrigin) {
|
|
93168
93468
|
const url = new URL(`${EMBED_PAGE_PATH}/${encodeURIComponent(slug)}`, hubOrigin);
|
|
@@ -93190,12 +93490,9 @@ class EmbedService {
|
|
|
93190
93490
|
return partner;
|
|
93191
93491
|
}
|
|
93192
93492
|
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));
|
|
93493
|
+
const game = this.deps.db.select({ id: games.id }).from(games).where(eq(games.slug, slug));
|
|
93494
|
+
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);
|
|
93495
|
+
return partner !== undefined;
|
|
93199
93496
|
}
|
|
93200
93497
|
async createLaunch(partner, input) {
|
|
93201
93498
|
const game = await this.deps.db.query.games.findFirst({
|
|
@@ -93206,9 +93503,8 @@ class EmbedService {
|
|
|
93206
93503
|
throw new NotFoundError("Game", input.gameSlug);
|
|
93207
93504
|
}
|
|
93208
93505
|
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");
|
|
93506
|
+
if (!canPartnerLaunch(partner, input.parentOrigin)) {
|
|
93507
|
+
throw new AccessDeniedError("This partner may not launch into that origin");
|
|
93212
93508
|
}
|
|
93213
93509
|
setAttributes({ "app.user.timeback_id": input.timebackId });
|
|
93214
93510
|
const user = await this.deps.resolveTimebackStudent(input.timebackId);
|
|
@@ -93270,20 +93566,13 @@ class EmbedService {
|
|
|
93270
93566
|
throw new NotFoundError("Embed launch", launchId);
|
|
93271
93567
|
}
|
|
93272
93568
|
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));
|
|
93569
|
+
const outcome = renewalRefusal(launch, () => canPartnerLaunch(partner, launch.parentOrigin));
|
|
93275
93570
|
setAttributes({ "app.embed.outcome": outcome ?? "renewed" });
|
|
93276
93571
|
if (outcome) {
|
|
93277
93572
|
throw new AccessDeniedError("This launch has ended");
|
|
93278
93573
|
}
|
|
93279
93574
|
return this.deps.mintGameToken(launch.gameId, launch.userId);
|
|
93280
93575
|
}
|
|
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
93576
|
async endLaunch(launchId) {
|
|
93288
93577
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
93289
93578
|
}
|
|
@@ -93300,15 +93589,12 @@ class EmbedService {
|
|
|
93300
93589
|
"app.embed.partner_id": launch.partnerId,
|
|
93301
93590
|
"app.user.id": launch.userId
|
|
93302
93591
|
});
|
|
93303
|
-
const
|
|
93304
|
-
|
|
93305
|
-
|
|
93306
|
-
|
|
93307
|
-
}),
|
|
93308
|
-
this.grantsFor(launch.partnerId, launch.gameId)
|
|
93309
|
-
]);
|
|
93592
|
+
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
93593
|
+
where: eq(embedPartners.id, launch.partnerId),
|
|
93594
|
+
columns: { enabled: true, allowedOrigins: true }
|
|
93595
|
+
});
|
|
93310
93596
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
93311
|
-
const stillAllowed = partner !== undefined && canPartnerLaunch(
|
|
93597
|
+
const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
|
|
93312
93598
|
if (!originMatches || !stillAllowed) {
|
|
93313
93599
|
await this.endLaunch(launch.id);
|
|
93314
93600
|
setAttributes({
|
|
@@ -93726,14 +94012,16 @@ function createStandaloneServices(deps) {
|
|
|
93726
94012
|
return { ...profile, timeback: timebackData };
|
|
93727
94013
|
}
|
|
93728
94014
|
});
|
|
94015
|
+
const embedPartner = new EmbedPartnerService({ db });
|
|
93729
94016
|
const verify = new VerifyService({
|
|
93730
94017
|
db,
|
|
93731
94018
|
validateGameToken: auth.validateGameToken.bind(auth)
|
|
93732
94019
|
});
|
|
93733
|
-
return { account, embed, lti, session, user, verify };
|
|
94020
|
+
return { account, embed, embedPartner, lti, session, user, verify };
|
|
93734
94021
|
}
|
|
93735
94022
|
var init_standalone = __esm(async () => {
|
|
93736
94023
|
init_spans();
|
|
94024
|
+
init_embed_partner_service();
|
|
93737
94025
|
init_embed_service();
|
|
93738
94026
|
init_lti_service();
|
|
93739
94027
|
init_session_service();
|
|
@@ -110178,7 +110466,7 @@ function ilike2(column6, value) {
|
|
|
110178
110466
|
function notIlike2(column6, value) {
|
|
110179
110467
|
return sql4`${column6} not ilike ${value}`;
|
|
110180
110468
|
}
|
|
110181
|
-
function
|
|
110469
|
+
function arrayContains2(column6, values) {
|
|
110182
110470
|
if (Array.isArray(values)) {
|
|
110183
110471
|
if (values.length === 0) {
|
|
110184
110472
|
throw new Error("arrayContains requires at least one value");
|
|
@@ -133497,7 +133785,7 @@ params: ${params}`);
|
|
|
133497
133785
|
and: () => and2,
|
|
133498
133786
|
applyMixins: () => applyMixins2,
|
|
133499
133787
|
arrayContained: () => arrayContained,
|
|
133500
|
-
arrayContains: () =>
|
|
133788
|
+
arrayContains: () => arrayContains2,
|
|
133501
133789
|
arrayOverlaps: () => arrayOverlaps,
|
|
133502
133790
|
asc: () => asc2,
|
|
133503
133791
|
avg: () => avg,
|
|
@@ -150067,15 +150355,36 @@ function requireUuidParam(value, name2) {
|
|
|
150067
150355
|
}
|
|
150068
150356
|
return value;
|
|
150069
150357
|
}
|
|
150358
|
+
function bearerToken(request) {
|
|
150359
|
+
const header = request.headers.get("Authorization");
|
|
150360
|
+
return header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
150361
|
+
}
|
|
150070
150362
|
async function requirePartner(ctx) {
|
|
150071
|
-
const
|
|
150072
|
-
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
150363
|
+
const token = bearerToken(ctx.request);
|
|
150073
150364
|
const verified = token ? await ctx.providers.auth.verifyEmbedPartnerToken(token) : null;
|
|
150074
150365
|
if (!verified) {
|
|
150075
150366
|
throw ApiError.unauthorized("A valid embed partner access token is required");
|
|
150076
150367
|
}
|
|
150077
150368
|
return ctx.services.embed.getPartnerByClientId(verified.clientId);
|
|
150078
150369
|
}
|
|
150370
|
+
async function requireEmbedAdmin(ctx) {
|
|
150371
|
+
const expected = ctx.config.embedAdminToken;
|
|
150372
|
+
if (!expected) {
|
|
150373
|
+
throw ApiError.serviceUnavailable("Embed partner management is not configured");
|
|
150374
|
+
}
|
|
150375
|
+
const token = bearerToken(ctx.request);
|
|
150376
|
+
const [given, wanted] = token ? await Promise.all([sha256Hex(token), sha256Hex(expected)]) : [];
|
|
150377
|
+
if (!given || given !== wanted) {
|
|
150378
|
+
throw ApiError.unauthorized("A valid embed admin token is required");
|
|
150379
|
+
}
|
|
150380
|
+
}
|
|
150381
|
+
function requireClientIdParam(ctx) {
|
|
150382
|
+
const clientId = EmbedPartnerClientIdSchema.safeParse(ctx.params.clientId);
|
|
150383
|
+
if (!clientId.success) {
|
|
150384
|
+
throw ApiError.badRequest("Missing or invalid clientId");
|
|
150385
|
+
}
|
|
150386
|
+
return clientId.data;
|
|
150387
|
+
}
|
|
150079
150388
|
async function createLaunch(ctx) {
|
|
150080
150389
|
const partner = await requirePartner(ctx);
|
|
150081
150390
|
const body2 = await parseRequestBody(ctx.request, CreateEmbedLaunchSchema);
|
|
@@ -150105,6 +150414,27 @@ async function redeemPass(ctx) {
|
|
|
150105
150414
|
timeback: launch.profile.timeback
|
|
150106
150415
|
};
|
|
150107
150416
|
}
|
|
150417
|
+
async function ensurePartner(ctx) {
|
|
150418
|
+
await requireEmbedAdmin(ctx);
|
|
150419
|
+
const clientId = requireClientIdParam(ctx);
|
|
150420
|
+
const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
|
|
150421
|
+
return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
|
|
150422
|
+
}
|
|
150423
|
+
async function addPartnerOrigin(ctx) {
|
|
150424
|
+
await requireEmbedAdmin(ctx);
|
|
150425
|
+
const clientId = requireClientIdParam(ctx);
|
|
150426
|
+
const body2 = await parseRequestBody(ctx.request, AddEmbedPartnerOriginSchema);
|
|
150427
|
+
return ctx.services.embedPartner.addOrigin(clientId, body2.origin);
|
|
150428
|
+
}
|
|
150429
|
+
async function removePartnerOrigin(ctx) {
|
|
150430
|
+
await requireEmbedAdmin(ctx);
|
|
150431
|
+
const clientId = requireClientIdParam(ctx);
|
|
150432
|
+
const origin = EmbedOriginSchema.safeParse(ctx.url.searchParams.get("origin"));
|
|
150433
|
+
if (!origin.success) {
|
|
150434
|
+
throw ApiError.badRequest("Missing or invalid origin");
|
|
150435
|
+
}
|
|
150436
|
+
return ctx.services.embedPartner.removeOrigin(clientId, origin.data);
|
|
150437
|
+
}
|
|
150108
150438
|
var embed2;
|
|
150109
150439
|
var init_embed_controller = __esm(() => {
|
|
150110
150440
|
init_src();
|
|
@@ -150114,8 +150444,11 @@ var init_embed_controller = __esm(() => {
|
|
|
150114
150444
|
init_utils7();
|
|
150115
150445
|
init_embed_util();
|
|
150116
150446
|
embed2 = defineControllerNames("embed", {
|
|
150447
|
+
addPartnerOrigin,
|
|
150117
150448
|
createLaunch,
|
|
150449
|
+
ensurePartner,
|
|
150118
150450
|
redeemPass,
|
|
150451
|
+
removePartnerOrigin,
|
|
150119
150452
|
renewToken
|
|
150120
150453
|
});
|
|
150121
150454
|
});
|
|
@@ -150722,7 +151055,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
150722
151055
|
limit
|
|
150723
151056
|
};
|
|
150724
151057
|
}
|
|
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;
|
|
151058
|
+
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
151059
|
var init_timeback_controller = __esm(() => {
|
|
150727
151060
|
init_esm();
|
|
150728
151061
|
init_src();
|
|
@@ -150801,6 +151134,27 @@ var init_timeback_controller = __esm(() => {
|
|
|
150801
151134
|
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
|
|
150802
151135
|
return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
|
|
150803
151136
|
});
|
|
151137
|
+
getHoleFillingCourses = requireGameManagementAccess(async (ctx) => {
|
|
151138
|
+
const gameId = ctx.params.gameId;
|
|
151139
|
+
if (!gameId) {
|
|
151140
|
+
throw ApiError.badRequest("Missing gameId");
|
|
151141
|
+
}
|
|
151142
|
+
if (!isValidUUID(gameId)) {
|
|
151143
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
151144
|
+
}
|
|
151145
|
+
return ctx.services.timeback.getHoleFillingCourses(gameId, ctx.user);
|
|
151146
|
+
});
|
|
151147
|
+
createHoleFillingCourse = requireDeveloper(async (ctx) => {
|
|
151148
|
+
const gameId = ctx.params.gameId;
|
|
151149
|
+
if (!gameId) {
|
|
151150
|
+
throw ApiError.badRequest("Missing gameId");
|
|
151151
|
+
}
|
|
151152
|
+
if (!isValidUUID(gameId)) {
|
|
151153
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
151154
|
+
}
|
|
151155
|
+
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackHoleFillingCourseRequestSchema);
|
|
151156
|
+
return ctx.services.timeback.createHoleFillingCourse(gameId, ctx.user, body2);
|
|
151157
|
+
});
|
|
150804
151158
|
updateIntegration = requireDeveloper(async (ctx) => {
|
|
150805
151159
|
const { gameId, courseId } = ctx.params;
|
|
150806
151160
|
if (!gameId || !courseId) {
|
|
@@ -151585,6 +151939,8 @@ var init_timeback_controller = __esm(() => {
|
|
|
151585
151939
|
getIntegrations,
|
|
151586
151940
|
getRemovedIntegrations,
|
|
151587
151941
|
createIntegration,
|
|
151942
|
+
getHoleFillingCourses,
|
|
151943
|
+
createHoleFillingCourse,
|
|
151588
151944
|
updateIntegration,
|
|
151589
151945
|
deactivateCourse,
|
|
151590
151946
|
reactivateCourse,
|