@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/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.2",
|
|
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,16 @@ 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
|
+
}
|
|
22872
22891
|
function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
|
|
22873
22892
|
return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
|
|
22874
22893
|
}
|
|
22875
|
-
|
|
22894
|
+
function isActiveBaseGameTimebackIntegration() {
|
|
22895
|
+
return sql`(${isActiveGameTimebackIntegrationStatus()} AND ${isBaseGameTimebackIntegration()})`;
|
|
22896
|
+
}
|
|
22897
|
+
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
22898
|
var init_helpers2 = __esm(() => {
|
|
22877
22899
|
init_drizzle_orm();
|
|
22878
22900
|
init_table7();
|
|
@@ -39449,12 +39471,12 @@ function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
|
|
|
39449
39471
|
function filterEnrollmentsByGame(enrollments, gameId) {
|
|
39450
39472
|
return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _, ...enrollment }) => enrollment);
|
|
39451
39473
|
}
|
|
39452
|
-
function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
39474
|
+
function mapEnrollmentsToUserEnrollments(enrollments, integrations, baseCourseIdByIntegrationId = new Map) {
|
|
39453
39475
|
const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
|
|
39454
39476
|
const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
|
|
39455
39477
|
return integrations.map((integration) => {
|
|
39456
39478
|
const enrollment = enrollmentByCourse.get(integration.courseId);
|
|
39457
|
-
|
|
39479
|
+
const userEnrollment = {
|
|
39458
39480
|
gameId: integration.gameId,
|
|
39459
39481
|
grade: integration.grade,
|
|
39460
39482
|
subject: integration.subject,
|
|
@@ -39462,6 +39484,17 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
39462
39484
|
orgId: courseToSchool.get(integration.courseId),
|
|
39463
39485
|
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
39464
39486
|
};
|
|
39487
|
+
if (integration.kind !== HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
39488
|
+
return userEnrollment;
|
|
39489
|
+
}
|
|
39490
|
+
const baseCourseId = integration.baseIntegrationId ? baseCourseIdByIntegrationId.get(integration.baseIntegrationId) : undefined;
|
|
39491
|
+
return {
|
|
39492
|
+
...userEnrollment,
|
|
39493
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
39494
|
+
...baseCourseId && { baseCourseId },
|
|
39495
|
+
...enrollment?.beginDate !== undefined && { beginDate: enrollment.beginDate },
|
|
39496
|
+
...enrollment?.metadata && { enrollmentMetadata: enrollment.metadata }
|
|
39497
|
+
};
|
|
39465
39498
|
});
|
|
39466
39499
|
}
|
|
39467
39500
|
function buildGameTimebackSummaries(integrations) {
|
|
@@ -39944,7 +39977,7 @@ var init_game_service = __esm(() => {
|
|
|
39944
39977
|
const db = this.deps.db;
|
|
39945
39978
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
39946
39979
|
columns: { gameId: true, subject: true },
|
|
39947
|
-
where:
|
|
39980
|
+
where: isActiveBaseGameTimebackIntegration(),
|
|
39948
39981
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
39949
39982
|
});
|
|
39950
39983
|
const subjectMap = {};
|
|
@@ -39968,9 +40001,7 @@ var init_game_service = __esm(() => {
|
|
|
39968
40001
|
}
|
|
39969
40002
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
39970
40003
|
columns: { gameId: true, subject: true, status: true },
|
|
39971
|
-
...accessibleGameIds
|
|
39972
|
-
where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
|
|
39973
|
-
},
|
|
40004
|
+
where: and(accessibleGameIds ? inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds]) : undefined, isBaseGameTimebackIntegration()),
|
|
39974
40005
|
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
39975
40006
|
});
|
|
39976
40007
|
return buildGameTimebackSummaries(integrations);
|
|
@@ -50401,7 +50432,7 @@ function rejectSystemManagedReview(input, context) {
|
|
|
50401
50432
|
});
|
|
50402
50433
|
}
|
|
50403
50434
|
}
|
|
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;
|
|
50435
|
+
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
50436
|
var init_schemas4 = __esm(() => {
|
|
50406
50437
|
init_esm();
|
|
50407
50438
|
init_src();
|
|
@@ -50437,6 +50468,26 @@ var init_schemas4 = __esm(() => {
|
|
|
50437
50468
|
masterableUnits: numberType().int().nonnegative(),
|
|
50438
50469
|
level: stringType().trim().min(1).optional()
|
|
50439
50470
|
});
|
|
50471
|
+
RESERVED_TIMEBACK_COURSE_SOURCED_IDS = new Set([
|
|
50472
|
+
"hole-filling",
|
|
50473
|
+
"removed",
|
|
50474
|
+
"components",
|
|
50475
|
+
"component-resources",
|
|
50476
|
+
"structure"
|
|
50477
|
+
]);
|
|
50478
|
+
TimebackCourseSourcedIdSchema = stringType().trim().min(3).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
|
|
50479
|
+
message: "Use letters, numbers, dots, dashes, or underscores, starting with a letter or number"
|
|
50480
|
+
}).refine((value) => !RESERVED_TIMEBACK_COURSE_SOURCED_IDS.has(value.toLowerCase()), {
|
|
50481
|
+
message: "This course ID is reserved"
|
|
50482
|
+
});
|
|
50483
|
+
CreateGameTimebackHoleFillingCourseRequestSchema = objectType({
|
|
50484
|
+
baseCourseId: stringType().trim().min(1),
|
|
50485
|
+
title: stringType().trim().min(1),
|
|
50486
|
+
courseCode: stringType().trim().min(1),
|
|
50487
|
+
totalXp: numberType().int().positive(),
|
|
50488
|
+
masterableUnits: numberType().int().nonnegative(),
|
|
50489
|
+
courseId: TimebackCourseSourcedIdSchema.optional()
|
|
50490
|
+
});
|
|
50440
50491
|
TimebackActivityDataSchema = objectType({
|
|
50441
50492
|
activityId: stringType().min(1),
|
|
50442
50493
|
activityName: stringType().optional(),
|
|
@@ -50768,21 +50819,20 @@ function isCanonicalOrigin(value) {
|
|
|
50768
50819
|
}
|
|
50769
50820
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
50770
50821
|
}
|
|
50771
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema,
|
|
50822
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
50772
50823
|
var init_schemas5 = __esm(() => {
|
|
50773
50824
|
init_esm();
|
|
50774
50825
|
LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
|
|
50775
50826
|
EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
|
|
50776
50827
|
message: "Origin must be a bare https origin like https://example.com, with no path or slash"
|
|
50777
50828
|
});
|
|
50778
|
-
|
|
50779
|
-
|
|
50780
|
-
|
|
50781
|
-
|
|
50782
|
-
|
|
50783
|
-
|
|
50829
|
+
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
50830
|
+
EnsureEmbedPartnerSchema = objectType({
|
|
50831
|
+
name: stringType().trim().min(1).max(100)
|
|
50832
|
+
}).strict();
|
|
50833
|
+
AddEmbedPartnerOriginSchema = objectType({
|
|
50834
|
+
origin: EmbedOriginSchema
|
|
50784
50835
|
}).strict();
|
|
50785
|
-
UpdateEmbedPartnerSchema = CreateEmbedPartnerSchema.omit({ clientId: true }).partial();
|
|
50786
50836
|
CreateEmbedLaunchSchema = objectType({
|
|
50787
50837
|
timebackId: stringType().trim().min(1).max(200),
|
|
50788
50838
|
gameSlug: stringType().trim().min(1).max(100),
|
|
@@ -77940,6 +77990,7 @@ class CourseAssessments {
|
|
|
77940
77990
|
}
|
|
77941
77991
|
function buildCoursePayload(config) {
|
|
77942
77992
|
return {
|
|
77993
|
+
...config.course.sourcedId && { sourcedId: config.course.sourcedId },
|
|
77943
77994
|
status: "active",
|
|
77944
77995
|
title: config.course.title,
|
|
77945
77996
|
courseCode: config.course.courseCode,
|
|
@@ -77986,6 +78037,16 @@ function buildComponentResourcePayload(config, sourcedIds) {
|
|
|
77986
78037
|
lessonType: config.componentResource.lessonType
|
|
77987
78038
|
};
|
|
77988
78039
|
}
|
|
78040
|
+
async function findOrUndefined(request) {
|
|
78041
|
+
try {
|
|
78042
|
+
return await request;
|
|
78043
|
+
} catch (error) {
|
|
78044
|
+
if (isApiError(error) && error.statusCode === 404) {
|
|
78045
|
+
return;
|
|
78046
|
+
}
|
|
78047
|
+
throw error;
|
|
78048
|
+
}
|
|
78049
|
+
}
|
|
77989
78050
|
|
|
77990
78051
|
class CourseIntegration {
|
|
77991
78052
|
core;
|
|
@@ -78024,6 +78085,32 @@ class CourseIntegration {
|
|
|
78024
78085
|
...verboseData && { verboseData }
|
|
78025
78086
|
};
|
|
78026
78087
|
}
|
|
78088
|
+
async findCourse(courseId) {
|
|
78089
|
+
return findOrUndefined(this.core.api.oneroster.courses.get(courseId));
|
|
78090
|
+
}
|
|
78091
|
+
async resumeSetup(courseId, config) {
|
|
78092
|
+
const oneroster = this.core.api.oneroster;
|
|
78093
|
+
const sourcedIds = deriveSourcedIds(courseId);
|
|
78094
|
+
if (!await findOrUndefined(oneroster.courses.getComponent(sourcedIds.component))) {
|
|
78095
|
+
await oneroster.courses.createComponent(buildComponentPayload(config, sourcedIds));
|
|
78096
|
+
addEvent("timeback.component_created", {
|
|
78097
|
+
"app.timeback.component_id": sourcedIds.component
|
|
78098
|
+
});
|
|
78099
|
+
}
|
|
78100
|
+
if (!await findOrUndefined(oneroster.resources.get(sourcedIds.resource))) {
|
|
78101
|
+
await oneroster.resources.create(buildResourcePayload(config, sourcedIds.resource));
|
|
78102
|
+
addEvent("timeback.resource_created", {
|
|
78103
|
+
"app.timeback.resource_id": sourcedIds.resource
|
|
78104
|
+
});
|
|
78105
|
+
}
|
|
78106
|
+
if (!await findOrUndefined(oneroster.courses.getComponentResource(sourcedIds.componentResource))) {
|
|
78107
|
+
await oneroster.courses.createComponentResource(buildComponentResourcePayload(config, sourcedIds));
|
|
78108
|
+
addEvent("timeback.component_resource_link_created", {
|
|
78109
|
+
"app.timeback.component_resource_id": sourcedIds.componentResource
|
|
78110
|
+
});
|
|
78111
|
+
}
|
|
78112
|
+
await this.update(courseId, config);
|
|
78113
|
+
}
|
|
78027
78114
|
async update(courseId, config) {
|
|
78028
78115
|
const oneroster = this.core.api.oneroster;
|
|
78029
78116
|
const sourcedIds = deriveSourcedIds(courseId);
|
|
@@ -78163,6 +78250,8 @@ function createCourseNamespace(core) {
|
|
|
78163
78250
|
update: (courseId, config) => integration.update(courseId, config),
|
|
78164
78251
|
verify: (courseId) => integration.verify(courseId),
|
|
78165
78252
|
getConfig: (courseId) => integration.getConfig(courseId),
|
|
78253
|
+
findCourse: (courseId) => integration.findCourse(courseId),
|
|
78254
|
+
resumeSetup: (courseId, config) => integration.resumeSetup(courseId, config),
|
|
78166
78255
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
78167
78256
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
78168
78257
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
@@ -78247,7 +78336,9 @@ class RosterEnrollments {
|
|
|
78247
78336
|
status: "active",
|
|
78248
78337
|
grades,
|
|
78249
78338
|
subjects,
|
|
78250
|
-
school: enrollment.school
|
|
78339
|
+
school: enrollment.school,
|
|
78340
|
+
beginDate: enrollment.beginDate,
|
|
78341
|
+
...enrollment.metadata && { metadata: enrollment.metadata }
|
|
78251
78342
|
};
|
|
78252
78343
|
});
|
|
78253
78344
|
this.core.caches.enrollments.set(studentId, enrollments);
|
|
@@ -89051,7 +89142,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
89051
89142
|
}) {
|
|
89052
89143
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
89053
89144
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
89054
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
89145
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
89055
89146
|
});
|
|
89056
89147
|
const matchingIntegrations = new Map;
|
|
89057
89148
|
for (const integration of integrations) {
|
|
@@ -89440,7 +89531,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
89440
89531
|
}) {
|
|
89441
89532
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
89442
89533
|
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
89443
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
89534
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
89444
89535
|
});
|
|
89445
89536
|
const orderedIntegrations = integrations.toSorted((left, right) => {
|
|
89446
89537
|
const subjectDifference = left.subject.localeCompare(right.subject);
|
|
@@ -89519,7 +89610,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
89519
89610
|
async candidateIntegrations(gameId, studentId, input, db = this.deps.db) {
|
|
89520
89611
|
const filters = [
|
|
89521
89612
|
eq(gameTimebackIntegrations.gameId, gameId),
|
|
89522
|
-
|
|
89613
|
+
isActiveBaseGameTimebackIntegration()
|
|
89523
89614
|
];
|
|
89524
89615
|
if (input.subject) {
|
|
89525
89616
|
filters.push(eq(gameTimebackIntegrations.subject, input.subject));
|
|
@@ -92461,6 +92552,50 @@ function timebackConfigMatchesCreateIntegrationRequest(config, request) {
|
|
|
92461
92552
|
const requestedLevel = request.level ?? deriveTimebackCourseLevelFromGrade(request.grade);
|
|
92462
92553
|
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
92554
|
}
|
|
92555
|
+
function buildHoleFillingCourseConfig(baseConfig, request) {
|
|
92556
|
+
const baseCourseMetadata = isRecord4(baseConfig.course.metadata) ? baseConfig.course.metadata : {};
|
|
92557
|
+
const baseMetrics = isRecord4(baseCourseMetadata.metrics) ? baseCourseMetadata.metrics : {};
|
|
92558
|
+
const baseResourceMetadata = isRecord4(baseConfig.resource.metadata) ? baseConfig.resource.metadata : {};
|
|
92559
|
+
const basePlaycademy = isPlaycademyResourceMetadata(baseResourceMetadata.playcademy) ? baseResourceMetadata.playcademy : undefined;
|
|
92560
|
+
return {
|
|
92561
|
+
organization: baseConfig.organization,
|
|
92562
|
+
course: {
|
|
92563
|
+
...baseConfig.course,
|
|
92564
|
+
...request.courseId && { sourcedId: request.courseId },
|
|
92565
|
+
title: request.title,
|
|
92566
|
+
courseCode: request.courseCode,
|
|
92567
|
+
metadata: {
|
|
92568
|
+
...baseCourseMetadata,
|
|
92569
|
+
courseType: HOLE_FILLING_COURSE_TYPE,
|
|
92570
|
+
baseCourseSourcedId: request.baseCourseId,
|
|
92571
|
+
metrics: {
|
|
92572
|
+
...baseMetrics,
|
|
92573
|
+
totalXp: request.totalXp,
|
|
92574
|
+
totalLessons: request.masterableUnits,
|
|
92575
|
+
totalGrades: 1
|
|
92576
|
+
}
|
|
92577
|
+
}
|
|
92578
|
+
},
|
|
92579
|
+
component: { ...baseConfig.component, title: `${request.title} Activities` },
|
|
92580
|
+
resource: {
|
|
92581
|
+
...baseConfig.resource,
|
|
92582
|
+
title: `${request.title} Game`,
|
|
92583
|
+
metadata: {
|
|
92584
|
+
...baseResourceMetadata,
|
|
92585
|
+
xp: request.totalXp,
|
|
92586
|
+
playcademy: {
|
|
92587
|
+
...basePlaycademy,
|
|
92588
|
+
mastery: {
|
|
92589
|
+
...basePlaycademy?.mastery,
|
|
92590
|
+
masterableUnits: request.masterableUnits
|
|
92591
|
+
}
|
|
92592
|
+
}
|
|
92593
|
+
}
|
|
92594
|
+
},
|
|
92595
|
+
componentResource: { ...baseConfig.componentResource, title: `${request.title} Lesson` }
|
|
92596
|
+
};
|
|
92597
|
+
}
|
|
92598
|
+
var HOLE_FILLING_COURSE_TYPE = "hole-filling";
|
|
92464
92599
|
var init_timeback_create_integration_util = __esm(() => {
|
|
92465
92600
|
init_constants2();
|
|
92466
92601
|
init_types2();
|
|
@@ -92476,7 +92611,7 @@ async function promoteCompletedCourse({
|
|
|
92476
92611
|
enrollments: prefetchedEnrollments
|
|
92477
92612
|
}) {
|
|
92478
92613
|
const subjectIntegrations = await db.query.gameTimebackIntegrations.findMany({
|
|
92479
|
-
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject),
|
|
92614
|
+
where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveBaseGameTimebackIntegration())
|
|
92480
92615
|
});
|
|
92481
92616
|
const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
|
|
92482
92617
|
if (!nextIntegration) {
|
|
@@ -92543,6 +92678,8 @@ function toGameTimebackIntegration(integration) {
|
|
|
92543
92678
|
courseId: integration.courseId,
|
|
92544
92679
|
grade: integration.grade,
|
|
92545
92680
|
subject: integration.subject,
|
|
92681
|
+
kind: integration.kind,
|
|
92682
|
+
baseIntegrationId: integration.baseIntegrationId,
|
|
92546
92683
|
totalXp: integration.totalXp ?? null,
|
|
92547
92684
|
status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
|
|
92548
92685
|
deactivatedAt: integration.deactivatedAt ?? null,
|
|
@@ -92580,6 +92717,9 @@ var init_timeback_removed_integration_util = __esm(() => {
|
|
|
92580
92717
|
|
|
92581
92718
|
// ../api-core/src/services/timeback.service.ts
|
|
92582
92719
|
import { createHash as createHash3 } from "node:crypto";
|
|
92720
|
+
function isUnregisteredHoleFillingOf(course, base) {
|
|
92721
|
+
return course.metadata?.courseType === HOLE_FILLING_COURSE_TYPE && course.metadata.baseCourseSourcedId === base.courseId;
|
|
92722
|
+
}
|
|
92583
92723
|
async function findGameTimebackIntegrationForUpdate(db, condition) {
|
|
92584
92724
|
const [integration] = await db.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
|
|
92585
92725
|
return integration;
|
|
@@ -92986,13 +93126,29 @@ var init_timeback_service = __esm(async () => {
|
|
|
92986
93126
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
92987
93127
|
where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
|
|
92988
93128
|
});
|
|
92989
|
-
return mapEnrollmentsToUserEnrollments(enrollments, integrations);
|
|
93129
|
+
return mapEnrollmentsToUserEnrollments(enrollments, integrations, await this.loadBaseCourseIds(integrations));
|
|
92990
93130
|
} catch (error) {
|
|
92991
93131
|
setAttribute("app.timeback.enrollment_fetch", "failed");
|
|
92992
93132
|
setAttribute("app.timeback.enrollment_fetch_error", errorMessage(error));
|
|
92993
93133
|
return [];
|
|
92994
93134
|
}
|
|
92995
93135
|
}
|
|
93136
|
+
async loadBaseCourseIds(integrations) {
|
|
93137
|
+
const courseIdById = new Map(integrations.map((row) => [row.id, row.courseId]));
|
|
93138
|
+
const missingBaseIds = [
|
|
93139
|
+
...new Set(integrations.map((row) => row.baseIntegrationId).filter((id) => id !== null && !courseIdById.has(id)))
|
|
93140
|
+
];
|
|
93141
|
+
if (missingBaseIds.length > 0) {
|
|
93142
|
+
const baseRows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93143
|
+
columns: { id: true, courseId: true },
|
|
93144
|
+
where: inArray(gameTimebackIntegrations.id, missingBaseIds)
|
|
93145
|
+
});
|
|
93146
|
+
for (const row of baseRows) {
|
|
93147
|
+
courseIdById.set(row.id, row.courseId);
|
|
93148
|
+
}
|
|
93149
|
+
}
|
|
93150
|
+
return courseIdById;
|
|
93151
|
+
}
|
|
92996
93152
|
async setupIntegration(gameId, request, user) {
|
|
92997
93153
|
return this.withClientTelemetry(async () => {
|
|
92998
93154
|
const client = this.requireClient();
|
|
@@ -93003,7 +93159,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93003
93159
|
"app.timeback.course_count": courses.length
|
|
93004
93160
|
});
|
|
93005
93161
|
const existing = await db.query.gameTimebackIntegrations.findMany({
|
|
93006
|
-
where: eq(gameTimebackIntegrations.gameId, gameId)
|
|
93162
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration())
|
|
93007
93163
|
});
|
|
93008
93164
|
const integrations = [];
|
|
93009
93165
|
const verboseData = [];
|
|
@@ -93151,13 +93307,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
93151
93307
|
"app.timeback.subject": request.subject
|
|
93152
93308
|
});
|
|
93153
93309
|
const activeConflict = await db.query.gameTimebackIntegrations.findFirst({
|
|
93154
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject),
|
|
93310
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveBaseGameTimebackIntegration())
|
|
93155
93311
|
});
|
|
93156
93312
|
if (activeConflict) {
|
|
93157
93313
|
throw new ConflictError(`A TimeBack integration already exists for ${request.subject} Grade ${request.grade}`);
|
|
93158
93314
|
}
|
|
93159
93315
|
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))
|
|
93316
|
+
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
93317
|
});
|
|
93162
93318
|
for (const candidate of removedCandidates) {
|
|
93163
93319
|
const config = await client.course.getConfig(candidate.courseId).catch((error) => {
|
|
@@ -93195,10 +93351,10 @@ var init_timeback_service = __esm(async () => {
|
|
|
93195
93351
|
}
|
|
93196
93352
|
}
|
|
93197
93353
|
const reference = await db.query.gameTimebackIntegrations.findFirst({
|
|
93198
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93354
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration()),
|
|
93199
93355
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
93200
93356
|
}) ?? await db.query.gameTimebackIntegrations.findFirst({
|
|
93201
|
-
where: eq(gameTimebackIntegrations.gameId, gameId),
|
|
93357
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration()),
|
|
93202
93358
|
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
93203
93359
|
});
|
|
93204
93360
|
if (!reference) {
|
|
@@ -93228,10 +93384,77 @@ var init_timeback_service = __esm(async () => {
|
|
|
93228
93384
|
return integration;
|
|
93229
93385
|
});
|
|
93230
93386
|
}
|
|
93387
|
+
async createHoleFillingCourse(gameId, user, request) {
|
|
93388
|
+
return this.withClientTelemetry(async () => {
|
|
93389
|
+
const client = this.requireClient();
|
|
93390
|
+
const db = this.deps.db;
|
|
93391
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93392
|
+
TimebackService.recordIntegrationOperation("create_hole_filling_course", {
|
|
93393
|
+
"app.timeback.base_course_id": request.baseCourseId,
|
|
93394
|
+
"app.timeback.custom_course_id": Boolean(request.courseId)
|
|
93395
|
+
});
|
|
93396
|
+
const integration = await db.transaction(async (tx) => {
|
|
93397
|
+
const database = tx;
|
|
93398
|
+
const base = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, request.baseCourseId), isActiveBaseGameTimebackIntegration()));
|
|
93399
|
+
if (!base) {
|
|
93400
|
+
throw new NotFoundError("Active base course", `${gameId}:${request.baseCourseId}`);
|
|
93401
|
+
}
|
|
93402
|
+
const config = buildHoleFillingCourseConfig(await client.course.getConfig(base.courseId), request);
|
|
93403
|
+
let courseId = request.courseId;
|
|
93404
|
+
if (courseId) {
|
|
93405
|
+
const takenLocally = await database.query.gameTimebackIntegrations.findFirst({
|
|
93406
|
+
columns: { id: true },
|
|
93407
|
+
where: eq(gameTimebackIntegrations.courseId, courseId)
|
|
93408
|
+
});
|
|
93409
|
+
const remote = takenLocally ? undefined : await client.course.findCourse(courseId);
|
|
93410
|
+
if (takenLocally || remote && !isUnregisteredHoleFillingOf(remote, base)) {
|
|
93411
|
+
throw new ConflictError(`A Timeback course with ID "${courseId}" already exists`);
|
|
93412
|
+
}
|
|
93413
|
+
if (remote) {
|
|
93414
|
+
setAttribute("app.timeback.course_create_mode", "resumed");
|
|
93415
|
+
await client.course.resumeSetup(courseId, config);
|
|
93416
|
+
} else {
|
|
93417
|
+
courseId = undefined;
|
|
93418
|
+
}
|
|
93419
|
+
}
|
|
93420
|
+
if (!courseId) {
|
|
93421
|
+
const created = await client.course.setup(config);
|
|
93422
|
+
courseId = created.courseId;
|
|
93423
|
+
}
|
|
93424
|
+
setAttribute("app.timeback.course_id", courseId);
|
|
93425
|
+
const [inserted] = await database.insert(gameTimebackIntegrations).values({
|
|
93426
|
+
gameId,
|
|
93427
|
+
courseId,
|
|
93428
|
+
grade: base.grade,
|
|
93429
|
+
subject: base.subject,
|
|
93430
|
+
totalXp: request.totalXp,
|
|
93431
|
+
kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
93432
|
+
baseIntegrationId: base.id
|
|
93433
|
+
}).returning();
|
|
93434
|
+
return inserted;
|
|
93435
|
+
});
|
|
93436
|
+
if (!integration) {
|
|
93437
|
+
throw new InternalError("Hole-filling course creation did not return an integration");
|
|
93438
|
+
}
|
|
93439
|
+
return toGameTimebackIntegration(integration);
|
|
93440
|
+
});
|
|
93441
|
+
}
|
|
93442
|
+
async getHoleFillingCourses(gameId, user) {
|
|
93443
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
93444
|
+
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93445
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.kind, HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND), isActiveGameTimebackIntegrationStatus()),
|
|
93446
|
+
orderBy: (table, { asc }) => [
|
|
93447
|
+
asc(table.subject),
|
|
93448
|
+
asc(table.grade),
|
|
93449
|
+
asc(table.createdAt)
|
|
93450
|
+
]
|
|
93451
|
+
});
|
|
93452
|
+
return rows.map((row) => toGameTimebackIntegration(row));
|
|
93453
|
+
}
|
|
93231
93454
|
async getIntegrations(gameId, user) {
|
|
93232
93455
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
93233
93456
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93234
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93457
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
93235
93458
|
});
|
|
93236
93459
|
return rows.map((row) => toGameTimebackIntegration(row));
|
|
93237
93460
|
}
|
|
@@ -93240,7 +93463,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93240
93463
|
const client = this.requireClient();
|
|
93241
93464
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
93242
93465
|
const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93243
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
|
|
93466
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration()),
|
|
93244
93467
|
orderBy: (table, { asc }) => [asc(table.subject), asc(table.grade)]
|
|
93245
93468
|
});
|
|
93246
93469
|
setAttribute("app.timeback.removed_integration_count", rows.length);
|
|
@@ -93271,6 +93494,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
93271
93494
|
if (!integration) {
|
|
93272
93495
|
throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
|
|
93273
93496
|
}
|
|
93497
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
93498
|
+
throw new ConflictError("This is a hole-filling course and may be referenced by Timeback, so it can't be removed.");
|
|
93499
|
+
}
|
|
93500
|
+
const activeHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
93501
|
+
columns: { id: true },
|
|
93502
|
+
where: and(eq(gameTimebackIntegrations.baseIntegrationId, integration.id), isActiveGameTimebackIntegrationStatus())
|
|
93503
|
+
});
|
|
93504
|
+
if (activeHoleFilling) {
|
|
93505
|
+
setAttribute("app.timeback.removal_blocked", "hole_filling_courses");
|
|
93506
|
+
throw new ConflictError("This course has hole-filling courses that may be referenced by Timeback, so it can't be removed.");
|
|
93507
|
+
}
|
|
93274
93508
|
const activeEnrollments = await client.roster.getCourseRoster(courseId, {
|
|
93275
93509
|
role: "student",
|
|
93276
93510
|
includeUsers: false
|
|
@@ -93308,11 +93542,18 @@ var init_timeback_service = __esm(async () => {
|
|
|
93308
93542
|
if (!integration) {
|
|
93309
93543
|
throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
|
|
93310
93544
|
}
|
|
93311
|
-
|
|
93312
|
-
|
|
93313
|
-
|
|
93314
|
-
|
|
93315
|
-
|
|
93545
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
93546
|
+
const base = integration.baseIntegrationId ? await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.id, integration.baseIntegrationId), isActiveBaseGameTimebackIntegration())) : undefined;
|
|
93547
|
+
if (!base) {
|
|
93548
|
+
throw new ConflictError("Restore the base course first: a hole-filling course can only be active beside its base course.");
|
|
93549
|
+
}
|
|
93550
|
+
} else {
|
|
93551
|
+
const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
93552
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveBaseGameTimebackIntegration())
|
|
93553
|
+
});
|
|
93554
|
+
if (activeConflict) {
|
|
93555
|
+
throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
|
|
93556
|
+
}
|
|
93316
93557
|
}
|
|
93317
93558
|
const now = new Date;
|
|
93318
93559
|
const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
|
|
@@ -93351,52 +93592,66 @@ var init_timeback_service = __esm(async () => {
|
|
|
93351
93592
|
TimebackService.recordIntegrationOperation("update_integration", {
|
|
93352
93593
|
"app.timeback.course_id": courseId
|
|
93353
93594
|
});
|
|
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())
|
|
93595
|
+
const updated = await this.deps.db.transaction(async (tx) => {
|
|
93596
|
+
const database = tx;
|
|
93597
|
+
const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
|
|
93598
|
+
if (!integration) {
|
|
93599
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
93600
|
+
}
|
|
93601
|
+
const timebackConfig = await client.course.getConfig(courseId);
|
|
93602
|
+
const liveSubject = timebackConfig.course.subjects[0];
|
|
93603
|
+
const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
|
|
93604
|
+
if (!isTimebackSubject(subject)) {
|
|
93605
|
+
throw new ValidationError(`Invalid subject "${subject}"`);
|
|
93606
|
+
}
|
|
93607
|
+
setAttributes({
|
|
93608
|
+
"app.timeback.grade": integration.grade,
|
|
93609
|
+
"app.timeback.previous_subject": integration.subject,
|
|
93610
|
+
"app.timeback.next_subject": subject
|
|
93374
93611
|
});
|
|
93375
|
-
if (
|
|
93376
|
-
|
|
93612
|
+
if (subject !== integration.subject) {
|
|
93613
|
+
if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
|
|
93614
|
+
throw new ValidationError("A hole-filling course must keep the subject of its base course");
|
|
93615
|
+
}
|
|
93616
|
+
const linkedHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
|
|
93617
|
+
columns: { id: true },
|
|
93618
|
+
where: eq(gameTimebackIntegrations.baseIntegrationId, integration.id)
|
|
93619
|
+
});
|
|
93620
|
+
if (linkedHoleFilling) {
|
|
93621
|
+
throw new ValidationError("This course has hole-filling courses, so its subject cannot change");
|
|
93622
|
+
}
|
|
93377
93623
|
}
|
|
93378
|
-
|
|
93379
|
-
|
|
93380
|
-
|
|
93381
|
-
|
|
93382
|
-
|
|
93383
|
-
|
|
93624
|
+
if (subject !== integration.subject) {
|
|
93625
|
+
const subjectConflict = await database.query.gameTimebackIntegrations.findFirst({
|
|
93626
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveBaseGameTimebackIntegration())
|
|
93627
|
+
});
|
|
93628
|
+
if (subjectConflict && subjectConflict.id !== integration.id) {
|
|
93629
|
+
throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
|
|
93630
|
+
}
|
|
93631
|
+
}
|
|
93632
|
+
const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
|
|
93633
|
+
const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
|
|
93634
|
+
setAttributes({
|
|
93635
|
+
"app.timeback.total_xp": totalXp,
|
|
93636
|
+
"app.timeback.masterable_units": masterableUnits
|
|
93637
|
+
});
|
|
93638
|
+
await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
|
|
93639
|
+
...patch,
|
|
93640
|
+
subject,
|
|
93641
|
+
totalXp,
|
|
93642
|
+
masterableUnits,
|
|
93643
|
+
grade: integration.grade
|
|
93644
|
+
}));
|
|
93645
|
+
const [row] = await database.update(gameTimebackIntegrations).set({
|
|
93646
|
+
subject,
|
|
93647
|
+
totalXp,
|
|
93648
|
+
updatedAt: new Date
|
|
93649
|
+
}).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
|
|
93650
|
+
if (!row) {
|
|
93651
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
93652
|
+
}
|
|
93653
|
+
return row;
|
|
93384
93654
|
});
|
|
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
93655
|
return toGameTimebackIntegration(updated);
|
|
93401
93656
|
});
|
|
93402
93657
|
}
|
|
@@ -93407,7 +93662,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93407
93662
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93408
93663
|
TimebackService.recordIntegrationOperation("verify_integration");
|
|
93409
93664
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
93410
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93665
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
93411
93666
|
});
|
|
93412
93667
|
if (integrations.length === 0) {
|
|
93413
93668
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -93445,7 +93700,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93445
93700
|
const client = this.requireClient();
|
|
93446
93701
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93447
93702
|
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
93448
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId),
|
|
93703
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
|
|
93449
93704
|
});
|
|
93450
93705
|
if (!integration) {
|
|
93451
93706
|
throw new NotFoundError("Timeback integration", gameId);
|
|
@@ -93459,17 +93714,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
93459
93714
|
const db = this.deps.db;
|
|
93460
93715
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93461
93716
|
TimebackService.recordIntegrationOperation("delete_integrations");
|
|
93462
|
-
|
|
93463
|
-
|
|
93717
|
+
await db.transaction(async (tx) => {
|
|
93718
|
+
const database = tx;
|
|
93719
|
+
await database.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId)).for("update");
|
|
93720
|
+
const integrations = await database.select().from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
93721
|
+
if (integrations.length === 0) {
|
|
93722
|
+
throw new NotFoundError("Timeback integration", gameId);
|
|
93723
|
+
}
|
|
93724
|
+
setAttribute("app.timeback.integration_count", integrations.length);
|
|
93725
|
+
const holeFillingCount = integrations.filter((integration) => integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND).length;
|
|
93726
|
+
if (holeFillingCount > 0) {
|
|
93727
|
+
setAttribute("app.timeback.hole_filling_count", holeFillingCount);
|
|
93728
|
+
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.`);
|
|
93729
|
+
}
|
|
93730
|
+
for (const integration of integrations) {
|
|
93731
|
+
await client.course.cleanup(integration.courseId);
|
|
93732
|
+
}
|
|
93733
|
+
await database.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
|
|
93464
93734
|
});
|
|
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
93735
|
});
|
|
93474
93736
|
}
|
|
93475
93737
|
static patchCourseMetadata(metadata2, totalXp, options) {
|
|
@@ -93649,7 +93911,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93649
93911
|
});
|
|
93650
93912
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93651
93913
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
93652
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject),
|
|
93914
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
93653
93915
|
});
|
|
93654
93916
|
if (!integration) {
|
|
93655
93917
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
@@ -93792,7 +94054,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93792
94054
|
const client = this.requireClient();
|
|
93793
94055
|
const db = this.deps.db;
|
|
93794
94056
|
const integrations = await db.query.gameTimebackIntegrations.findMany({
|
|
93795
|
-
where:
|
|
94057
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), subject ? eq(gameTimebackIntegrations.subject, subject) : undefined, isActiveBaseGameTimebackIntegration())
|
|
93796
94058
|
});
|
|
93797
94059
|
if (integrations.length === 0) {
|
|
93798
94060
|
throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
|
|
@@ -93981,7 +94243,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93981
94243
|
}
|
|
93982
94244
|
const pendingHeartbeat = (async () => {
|
|
93983
94245
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
93984
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject),
|
|
94246
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
93985
94247
|
});
|
|
93986
94248
|
if (!integration) {
|
|
93987
94249
|
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
@@ -94111,7 +94373,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
94111
94373
|
const db = this.deps.db;
|
|
94112
94374
|
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
94113
94375
|
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
94114
|
-
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject),
|
|
94376
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveBaseGameTimebackIntegration())
|
|
94115
94377
|
});
|
|
94116
94378
|
if (!integration) {
|
|
94117
94379
|
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
@@ -94980,9 +95242,47 @@ var init_account_service = __esm(async () => {
|
|
|
94980
95242
|
await init_errors8();
|
|
94981
95243
|
});
|
|
94982
95244
|
|
|
95245
|
+
// ../api-core/src/services/embed-partner.service.ts
|
|
95246
|
+
class EmbedPartnerService {
|
|
95247
|
+
deps;
|
|
95248
|
+
constructor(deps) {
|
|
95249
|
+
this.deps = deps;
|
|
95250
|
+
}
|
|
95251
|
+
async ensurePartner(clientId, name2) {
|
|
95252
|
+
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
|
|
95253
|
+
target: embedPartners.clientId,
|
|
95254
|
+
set: { name: name2, updatedAt: new Date }
|
|
95255
|
+
}).returning();
|
|
95256
|
+
if (!partner) {
|
|
95257
|
+
throw new InternalError("Failed to ensure embed partner");
|
|
95258
|
+
}
|
|
95259
|
+
return partner;
|
|
95260
|
+
}
|
|
95261
|
+
addOrigin(clientId, origin) {
|
|
95262
|
+
return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
|
|
95263
|
+
THEN ${embedPartners.allowedOrigins}
|
|
95264
|
+
ELSE array_append(${embedPartners.allowedOrigins}, ${origin}) END`);
|
|
95265
|
+
}
|
|
95266
|
+
removeOrigin(clientId, origin) {
|
|
95267
|
+
return this.setOrigins(clientId, sql`array_remove(${embedPartners.allowedOrigins}, ${origin})`);
|
|
95268
|
+
}
|
|
95269
|
+
async setOrigins(clientId, origins) {
|
|
95270
|
+
const [partner] = await this.deps.db.update(embedPartners).set({ allowedOrigins: origins, updatedAt: new Date }).where(eq(embedPartners.clientId, clientId)).returning();
|
|
95271
|
+
if (!partner) {
|
|
95272
|
+
throw new NotFoundError("Embed partner", clientId);
|
|
95273
|
+
}
|
|
95274
|
+
return partner;
|
|
95275
|
+
}
|
|
95276
|
+
}
|
|
95277
|
+
var init_embed_partner_service = __esm(() => {
|
|
95278
|
+
init_drizzle_orm();
|
|
95279
|
+
init_tables_index();
|
|
95280
|
+
init_errors2();
|
|
95281
|
+
});
|
|
95282
|
+
|
|
94983
95283
|
// ../api-core/src/utils/embed.util.ts
|
|
94984
|
-
function canPartnerLaunch(partner, origin
|
|
94985
|
-
return partner.enabled && partner.allowedOrigins.includes(origin)
|
|
95284
|
+
function canPartnerLaunch(partner, origin) {
|
|
95285
|
+
return partner.enabled && partner.allowedOrigins.includes(origin);
|
|
94986
95286
|
}
|
|
94987
95287
|
function embedPageUrl(hubOrigin, slug, parentOrigin) {
|
|
94988
95288
|
const url = new URL(`${EMBED_PAGE_PATH}/${encodeURIComponent(slug)}`, hubOrigin);
|
|
@@ -95010,12 +95310,9 @@ class EmbedService {
|
|
|
95010
95310
|
return partner;
|
|
95011
95311
|
}
|
|
95012
95312
|
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));
|
|
95313
|
+
const game = this.deps.db.select({ id: games.id }).from(games).where(eq(games.slug, slug));
|
|
95314
|
+
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);
|
|
95315
|
+
return partner !== undefined;
|
|
95019
95316
|
}
|
|
95020
95317
|
async createLaunch(partner, input) {
|
|
95021
95318
|
const game = await this.deps.db.query.games.findFirst({
|
|
@@ -95026,9 +95323,8 @@ class EmbedService {
|
|
|
95026
95323
|
throw new NotFoundError("Game", input.gameSlug);
|
|
95027
95324
|
}
|
|
95028
95325
|
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");
|
|
95326
|
+
if (!canPartnerLaunch(partner, input.parentOrigin)) {
|
|
95327
|
+
throw new AccessDeniedError("This partner may not launch into that origin");
|
|
95032
95328
|
}
|
|
95033
95329
|
setAttributes({ "app.user.timeback_id": input.timebackId });
|
|
95034
95330
|
const user = await this.deps.resolveTimebackStudent(input.timebackId);
|
|
@@ -95090,20 +95386,13 @@ class EmbedService {
|
|
|
95090
95386
|
throw new NotFoundError("Embed launch", launchId);
|
|
95091
95387
|
}
|
|
95092
95388
|
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));
|
|
95389
|
+
const outcome = renewalRefusal(launch, () => canPartnerLaunch(partner, launch.parentOrigin));
|
|
95095
95390
|
setAttributes({ "app.embed.outcome": outcome ?? "renewed" });
|
|
95096
95391
|
if (outcome) {
|
|
95097
95392
|
throw new AccessDeniedError("This launch has ended");
|
|
95098
95393
|
}
|
|
95099
95394
|
return this.deps.mintGameToken(launch.gameId, launch.userId);
|
|
95100
95395
|
}
|
|
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
95396
|
async endLaunch(launchId) {
|
|
95108
95397
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
95109
95398
|
}
|
|
@@ -95120,15 +95409,12 @@ class EmbedService {
|
|
|
95120
95409
|
"app.embed.partner_id": launch.partnerId,
|
|
95121
95410
|
"app.user.id": launch.userId
|
|
95122
95411
|
});
|
|
95123
|
-
const
|
|
95124
|
-
|
|
95125
|
-
|
|
95126
|
-
|
|
95127
|
-
}),
|
|
95128
|
-
this.grantsFor(launch.partnerId, launch.gameId)
|
|
95129
|
-
]);
|
|
95412
|
+
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
95413
|
+
where: eq(embedPartners.id, launch.partnerId),
|
|
95414
|
+
columns: { enabled: true, allowedOrigins: true }
|
|
95415
|
+
});
|
|
95130
95416
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
95131
|
-
const stillAllowed = partner !== undefined && canPartnerLaunch(
|
|
95417
|
+
const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
|
|
95132
95418
|
if (!originMatches || !stillAllowed) {
|
|
95133
95419
|
await this.endLaunch(launch.id);
|
|
95134
95420
|
setAttributes({
|
|
@@ -95546,14 +95832,16 @@ function createStandaloneServices(deps) {
|
|
|
95546
95832
|
return { ...profile, timeback: timebackData };
|
|
95547
95833
|
}
|
|
95548
95834
|
});
|
|
95835
|
+
const embedPartner = new EmbedPartnerService({ db });
|
|
95549
95836
|
const verify = new VerifyService({
|
|
95550
95837
|
db,
|
|
95551
95838
|
validateGameToken: auth.validateGameToken.bind(auth)
|
|
95552
95839
|
});
|
|
95553
|
-
return { account, embed, lti, session, user, verify };
|
|
95840
|
+
return { account, embed, embedPartner, lti, session, user, verify };
|
|
95554
95841
|
}
|
|
95555
95842
|
var init_standalone = __esm(async () => {
|
|
95556
95843
|
init_spans();
|
|
95844
|
+
init_embed_partner_service();
|
|
95557
95845
|
init_embed_service();
|
|
95558
95846
|
init_lti_service();
|
|
95559
95847
|
init_session_service();
|
|
@@ -111998,7 +112286,7 @@ function ilike2(column6, value) {
|
|
|
111998
112286
|
function notIlike2(column6, value) {
|
|
111999
112287
|
return sql4`${column6} not ilike ${value}`;
|
|
112000
112288
|
}
|
|
112001
|
-
function
|
|
112289
|
+
function arrayContains2(column6, values) {
|
|
112002
112290
|
if (Array.isArray(values)) {
|
|
112003
112291
|
if (values.length === 0) {
|
|
112004
112292
|
throw new Error("arrayContains requires at least one value");
|
|
@@ -135317,7 +135605,7 @@ params: ${params}`);
|
|
|
135317
135605
|
and: () => and2,
|
|
135318
135606
|
applyMixins: () => applyMixins2,
|
|
135319
135607
|
arrayContained: () => arrayContained,
|
|
135320
|
-
arrayContains: () =>
|
|
135608
|
+
arrayContains: () => arrayContains2,
|
|
135321
135609
|
arrayOverlaps: () => arrayOverlaps,
|
|
135322
135610
|
asc: () => asc2,
|
|
135323
135611
|
avg: () => avg,
|
|
@@ -151887,15 +152175,36 @@ function requireUuidParam(value, name2) {
|
|
|
151887
152175
|
}
|
|
151888
152176
|
return value;
|
|
151889
152177
|
}
|
|
152178
|
+
function bearerToken(request) {
|
|
152179
|
+
const header = request.headers.get("Authorization");
|
|
152180
|
+
return header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
152181
|
+
}
|
|
151890
152182
|
async function requirePartner(ctx) {
|
|
151891
|
-
const
|
|
151892
|
-
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
152183
|
+
const token = bearerToken(ctx.request);
|
|
151893
152184
|
const verified = token ? await ctx.providers.auth.verifyEmbedPartnerToken(token) : null;
|
|
151894
152185
|
if (!verified) {
|
|
151895
152186
|
throw ApiError.unauthorized("A valid embed partner access token is required");
|
|
151896
152187
|
}
|
|
151897
152188
|
return ctx.services.embed.getPartnerByClientId(verified.clientId);
|
|
151898
152189
|
}
|
|
152190
|
+
async function requireEmbedAdmin(ctx) {
|
|
152191
|
+
const expected = ctx.config.embedAdminToken;
|
|
152192
|
+
if (!expected) {
|
|
152193
|
+
throw ApiError.serviceUnavailable("Embed partner management is not configured");
|
|
152194
|
+
}
|
|
152195
|
+
const token = bearerToken(ctx.request);
|
|
152196
|
+
const [given, wanted] = token ? await Promise.all([sha256Hex(token), sha256Hex(expected)]) : [];
|
|
152197
|
+
if (!given || given !== wanted) {
|
|
152198
|
+
throw ApiError.unauthorized("A valid embed admin token is required");
|
|
152199
|
+
}
|
|
152200
|
+
}
|
|
152201
|
+
function requireClientIdParam(ctx) {
|
|
152202
|
+
const clientId = EmbedPartnerClientIdSchema.safeParse(ctx.params.clientId);
|
|
152203
|
+
if (!clientId.success) {
|
|
152204
|
+
throw ApiError.badRequest("Missing or invalid clientId");
|
|
152205
|
+
}
|
|
152206
|
+
return clientId.data;
|
|
152207
|
+
}
|
|
151899
152208
|
async function createLaunch(ctx) {
|
|
151900
152209
|
const partner = await requirePartner(ctx);
|
|
151901
152210
|
const body2 = await parseRequestBody(ctx.request, CreateEmbedLaunchSchema);
|
|
@@ -151925,6 +152234,27 @@ async function redeemPass(ctx) {
|
|
|
151925
152234
|
timeback: launch.profile.timeback
|
|
151926
152235
|
};
|
|
151927
152236
|
}
|
|
152237
|
+
async function ensurePartner(ctx) {
|
|
152238
|
+
await requireEmbedAdmin(ctx);
|
|
152239
|
+
const clientId = requireClientIdParam(ctx);
|
|
152240
|
+
const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
|
|
152241
|
+
return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
|
|
152242
|
+
}
|
|
152243
|
+
async function addPartnerOrigin(ctx) {
|
|
152244
|
+
await requireEmbedAdmin(ctx);
|
|
152245
|
+
const clientId = requireClientIdParam(ctx);
|
|
152246
|
+
const body2 = await parseRequestBody(ctx.request, AddEmbedPartnerOriginSchema);
|
|
152247
|
+
return ctx.services.embedPartner.addOrigin(clientId, body2.origin);
|
|
152248
|
+
}
|
|
152249
|
+
async function removePartnerOrigin(ctx) {
|
|
152250
|
+
await requireEmbedAdmin(ctx);
|
|
152251
|
+
const clientId = requireClientIdParam(ctx);
|
|
152252
|
+
const origin = EmbedOriginSchema.safeParse(ctx.url.searchParams.get("origin"));
|
|
152253
|
+
if (!origin.success) {
|
|
152254
|
+
throw ApiError.badRequest("Missing or invalid origin");
|
|
152255
|
+
}
|
|
152256
|
+
return ctx.services.embedPartner.removeOrigin(clientId, origin.data);
|
|
152257
|
+
}
|
|
151928
152258
|
var embed2;
|
|
151929
152259
|
var init_embed_controller = __esm(() => {
|
|
151930
152260
|
init_src();
|
|
@@ -151934,8 +152264,11 @@ var init_embed_controller = __esm(() => {
|
|
|
151934
152264
|
init_utils7();
|
|
151935
152265
|
init_embed_util();
|
|
151936
152266
|
embed2 = defineControllerNames("embed", {
|
|
152267
|
+
addPartnerOrigin,
|
|
151937
152268
|
createLaunch,
|
|
152269
|
+
ensurePartner,
|
|
151938
152270
|
redeemPass,
|
|
152271
|
+
removePartnerOrigin,
|
|
151939
152272
|
renewToken
|
|
151940
152273
|
});
|
|
151941
152274
|
});
|
|
@@ -152542,7 +152875,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
152542
152875
|
limit
|
|
152543
152876
|
};
|
|
152544
152877
|
}
|
|
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;
|
|
152878
|
+
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
152879
|
var init_timeback_controller = __esm(() => {
|
|
152547
152880
|
init_esm();
|
|
152548
152881
|
init_src();
|
|
@@ -152621,6 +152954,27 @@ var init_timeback_controller = __esm(() => {
|
|
|
152621
152954
|
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
|
|
152622
152955
|
return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
|
|
152623
152956
|
});
|
|
152957
|
+
getHoleFillingCourses = requireGameManagementAccess(async (ctx) => {
|
|
152958
|
+
const gameId = ctx.params.gameId;
|
|
152959
|
+
if (!gameId) {
|
|
152960
|
+
throw ApiError.badRequest("Missing gameId");
|
|
152961
|
+
}
|
|
152962
|
+
if (!isValidUUID(gameId)) {
|
|
152963
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
152964
|
+
}
|
|
152965
|
+
return ctx.services.timeback.getHoleFillingCourses(gameId, ctx.user);
|
|
152966
|
+
});
|
|
152967
|
+
createHoleFillingCourse = requireDeveloper(async (ctx) => {
|
|
152968
|
+
const gameId = ctx.params.gameId;
|
|
152969
|
+
if (!gameId) {
|
|
152970
|
+
throw ApiError.badRequest("Missing gameId");
|
|
152971
|
+
}
|
|
152972
|
+
if (!isValidUUID(gameId)) {
|
|
152973
|
+
throw ApiError.unprocessableEntity("Invalid gameId format");
|
|
152974
|
+
}
|
|
152975
|
+
const body2 = await parseRequestBody(ctx.request, CreateGameTimebackHoleFillingCourseRequestSchema);
|
|
152976
|
+
return ctx.services.timeback.createHoleFillingCourse(gameId, ctx.user, body2);
|
|
152977
|
+
});
|
|
152624
152978
|
updateIntegration = requireDeveloper(async (ctx) => {
|
|
152625
152979
|
const { gameId, courseId } = ctx.params;
|
|
152626
152980
|
if (!gameId || !courseId) {
|
|
@@ -153405,6 +153759,8 @@ var init_timeback_controller = __esm(() => {
|
|
|
153405
153759
|
getIntegrations,
|
|
153406
153760
|
getRemovedIntegrations,
|
|
153407
153761
|
createIntegration,
|
|
153762
|
+
getHoleFillingCourses,
|
|
153763
|
+
createHoleFillingCourse,
|
|
153408
153764
|
updateIntegration,
|
|
153409
153765
|
deactivateCourse,
|
|
153410
153766
|
reactivateCourse,
|