@playcademy/sandbox 0.11.1-beta.1 → 0.11.1-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/README.md +40 -20
- package/dist/cli.js +18 -53
- package/dist/mocks/timeback.d.ts +1 -1
- package/dist/server.js +18 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,17 +199,18 @@ bunx @playcademy/sandbox \
|
|
|
199
199
|
--timeback-student-id your-student-sourcedId
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
| Option
|
|
203
|
-
|
|
|
204
|
-
| `--timeback-local`
|
|
205
|
-
| `--timeback-oneroster-url <url>`
|
|
206
|
-
| `--timeback-caliper-url <url>`
|
|
207
|
-
| `--timeback-course-id <id>`
|
|
208
|
-
| `--timeback-student-id <id>`
|
|
209
|
-
| `--timeback-role <role>`
|
|
210
|
-
| `--timeback-org-id <id>`
|
|
211
|
-
| `--timeback-org-name <name>`
|
|
212
|
-
| `--timeback-org-type <type>`
|
|
202
|
+
| Option | Description |
|
|
203
|
+
| :-------------------------------------- | :-------------------------------------------------------------------------------- |
|
|
204
|
+
| `--timeback-local` | Enable local TimeBack mode (Docker) |
|
|
205
|
+
| `--timeback-oneroster-url <url>` | OneRoster API URL |
|
|
206
|
+
| `--timeback-caliper-url <url>` | Caliper API URL |
|
|
207
|
+
| `--timeback-course-id <id>` | Course ID for enrollments |
|
|
208
|
+
| `--timeback-student-id <id>` | Student ID to link to demo user |
|
|
209
|
+
| `--timeback-role <role>` | User role (student, parent, teacher, administrator) |
|
|
210
|
+
| `--timeback-org-id <id>` | Organization ID |
|
|
211
|
+
| `--timeback-org-name <name>` | Organization display name |
|
|
212
|
+
| `--timeback-org-type <type>` | Organization type (school, district, department, etc.) |
|
|
213
|
+
| `--timeback-excluded-courses <courses>` | Comma-separated courses to exclude from enrollment (e.g. `FastMath:3,FastMath:4`) |
|
|
213
214
|
|
|
214
215
|
### Environment Variables
|
|
215
216
|
|
|
@@ -262,15 +263,34 @@ The sandbox provides these TimeBack endpoints (matching production platform):
|
|
|
262
263
|
**Runtime:**
|
|
263
264
|
|
|
264
265
|
- `POST /api/timeback/end-activity` - Submit activity results
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
- `
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
266
|
+
- `POST /api/timeback/heartbeat` - Record incremental activity heartbeat
|
|
267
|
+
- `POST /api/timeback/advance-course` - Advance a student to the next-grade course
|
|
268
|
+
- `POST /api/timeback/unenroll-course` - Unenroll a student from their active course
|
|
269
|
+
- `POST /api/timeback/populate-student` - No-op in the sandbox (always returns `no_record`)
|
|
270
|
+
|
|
271
|
+
**Assessments:**
|
|
272
|
+
|
|
273
|
+
- `POST /api/timeback/assessments/prepare` - Prepare the next assessment attempt
|
|
274
|
+
- `POST /api/timeback/assessments/start` - Start a new assessment attempt
|
|
275
|
+
- `GET /api/timeback/assessments/latest` - Get the user's latest attempt
|
|
276
|
+
- `GET /api/timeback/assessments/:attemptId` - Get a specific attempt
|
|
277
|
+
- `POST /api/timeback/assessments/:attemptId/resume` - Resume an attempt
|
|
278
|
+
- `POST /api/timeback/assessments/:attemptId/progress` - Record mastery progress
|
|
279
|
+
- `POST /api/timeback/assessments/:attemptId/save` - Checkpoint an in-progress attempt
|
|
280
|
+
- `POST /api/timeback/assessments/:attemptId/submit-item` - Score a single item
|
|
281
|
+
- `POST /api/timeback/assessments/:attemptId/submit` - Submit an attempt
|
|
282
|
+
- `POST /api/timeback/assessments/:attemptId/finalize` - Record XP and publish completion
|
|
283
|
+
|
|
284
|
+
**Student Data:**
|
|
285
|
+
|
|
286
|
+
- `GET /api/timeback/user` - Get the current user's TimeBack data
|
|
287
|
+
- `GET /api/timeback/user/enrollments` - Get the current user's enrollments
|
|
288
|
+
- `GET /api/timeback/user/:timebackId` - Get a student's TimeBack data
|
|
289
|
+
- `GET /api/timeback/student-xp/:timebackId` - Get a student's XP
|
|
290
|
+
- `GET /api/timeback/student-mastery/:timebackId` - Get a student's mastery
|
|
291
|
+
- `GET /api/timeback/student-highest-grade-mastered/:timebackId` - Get a student's highest grade mastered
|
|
292
|
+
|
|
293
|
+
These use the same api-core handlers as the production platform, ensuring consistency. The student data routes return mock data when TimeBack runs in mock mode.
|
|
274
294
|
|
|
275
295
|
When properly configured, your game can call `client.timeback.endActivity()` during local development and see real events appear in your TimeBack dashboard.
|
|
276
296
|
|
package/dist/cli.js
CHANGED
|
@@ -1142,7 +1142,7 @@ var package_default;
|
|
|
1142
1142
|
var init_package = __esm(() => {
|
|
1143
1143
|
package_default = {
|
|
1144
1144
|
name: "@playcademy/sandbox",
|
|
1145
|
-
version: "0.11.1-beta.
|
|
1145
|
+
version: "0.11.1-beta.2",
|
|
1146
1146
|
description: "Local development server for Playcademy game development",
|
|
1147
1147
|
type: "module",
|
|
1148
1148
|
exports: {
|
|
@@ -110367,12 +110367,13 @@ class LeaderboardService {
|
|
|
110367
110367
|
eq(gameScores.gameId, gameId),
|
|
110368
110368
|
eq(users.isAnonymous, isAnonymousUser)
|
|
110369
110369
|
];
|
|
110370
|
-
const bestScoresSubquery = db2.
|
|
110370
|
+
const bestScoresSubquery = db2.selectDistinctOn([gameScores.userId, gameScores.gameId], {
|
|
110371
110371
|
userId: gameScores.userId,
|
|
110372
110372
|
gameId: gameScores.gameId,
|
|
110373
|
-
score:
|
|
110374
|
-
achievedAt:
|
|
110375
|
-
|
|
110373
|
+
score: gameScores.score,
|
|
110374
|
+
achievedAt: gameScores.achievedAt,
|
|
110375
|
+
metadata: gameScores.metadata
|
|
110376
|
+
}).from(gameScores).innerJoin(users, eq(gameScores.userId, users.id)).where(and(...conditions2)).orderBy(gameScores.userId, gameScores.gameId, desc(gameScores.score), asc(gameScores.achievedAt)).as("best_scores");
|
|
110376
110377
|
const scores = await db2.select({
|
|
110377
110378
|
userId: users.id,
|
|
110378
110379
|
username: users.username,
|
|
@@ -110380,11 +110381,11 @@ class LeaderboardService {
|
|
|
110380
110381
|
userImage: users.image,
|
|
110381
110382
|
score: bestScoresSubquery.score,
|
|
110382
110383
|
achievedAt: bestScoresSubquery.achievedAt,
|
|
110383
|
-
metadata:
|
|
110384
|
+
metadata: bestScoresSubquery.metadata,
|
|
110384
110385
|
gameId: games.id,
|
|
110385
110386
|
gameTitle: games.displayName,
|
|
110386
110387
|
gameSlug: games.slug
|
|
110387
|
-
}).from(bestScoresSubquery).innerJoin(users, eq(bestScoresSubquery.userId, users.id)).innerJoin(games, eq(bestScoresSubquery.gameId, games.id)).
|
|
110388
|
+
}).from(bestScoresSubquery).innerJoin(users, eq(bestScoresSubquery.userId, users.id)).innerJoin(games, eq(bestScoresSubquery.gameId, games.id)).orderBy(desc(bestScoresSubquery.score), asc(bestScoresSubquery.achievedAt), asc(bestScoresSubquery.userId)).limit(limit).offset(offset);
|
|
110388
110389
|
setAttributes({
|
|
110389
110390
|
"app.leaderboard.result_count": scores.length,
|
|
110390
110391
|
"app.leaderboard.no_result": scores.length === 0
|
|
@@ -110624,7 +110625,7 @@ class UserService {
|
|
|
110624
110625
|
"app.user.timeback_enriched": false
|
|
110625
110626
|
});
|
|
110626
110627
|
const localDay = await this.resolveLocalDay(userData, observedTimeZone);
|
|
110627
|
-
const timeback2 = userData.timebackId ? await this.
|
|
110628
|
+
const timeback2 = userData.timebackId ? await this.fetchTimebackProfile(userData.timebackId) : undefined;
|
|
110628
110629
|
setAttribute("app.user.timeback_enriched", Boolean(timeback2));
|
|
110629
110630
|
if (gameId) {
|
|
110630
110631
|
return {
|
|
@@ -110729,18 +110730,10 @@ class UserService {
|
|
|
110729
110730
|
observedAt: null
|
|
110730
110731
|
};
|
|
110731
110732
|
}
|
|
110732
|
-
async
|
|
110733
|
-
const
|
|
110734
|
-
|
|
110735
|
-
|
|
110736
|
-
]);
|
|
110737
|
-
const enrollments = gameId ? filterEnrollmentsByGame(allEnrollments, gameId) : allEnrollments;
|
|
110738
|
-
const organizations = gameId ? this.filterOrganizationsByEnrollments(allOrganizations, enrollments) : allOrganizations;
|
|
110739
|
-
setAttributes({
|
|
110740
|
-
"app.timeback.enrollment_count": enrollments.length,
|
|
110741
|
-
"app.timeback.organization_count": organizations.length
|
|
110742
|
-
});
|
|
110743
|
-
return { id: timebackId, role, enrollments, organizations };
|
|
110733
|
+
async fetchTimebackProfile(timebackId) {
|
|
110734
|
+
const { role, organizations } = await withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId));
|
|
110735
|
+
setAttribute("app.timeback.organization_count", organizations.length);
|
|
110736
|
+
return { id: timebackId, role, organizations };
|
|
110744
110737
|
}
|
|
110745
110738
|
async fetchStudentProfile(timebackId) {
|
|
110746
110739
|
if (!this.deps.timeback) {
|
|
@@ -110779,46 +110772,14 @@ class UserService {
|
|
|
110779
110772
|
return { role: "student", organizations: [] };
|
|
110780
110773
|
}
|
|
110781
110774
|
}
|
|
110782
|
-
async fetchEnrollments(timebackId) {
|
|
110783
|
-
if (!this.deps.timeback) {
|
|
110784
|
-
setAttribute("app.timeback.client", "unavailable");
|
|
110785
|
-
return [];
|
|
110786
|
-
}
|
|
110787
|
-
try {
|
|
110788
|
-
const enrollments = await this.deps.timeback.roster.getEnrollments(timebackId);
|
|
110789
|
-
const courseIds = enrollments.map((e) => e.courseId).filter((id) => Boolean(id));
|
|
110790
|
-
if (courseIds.length === 0) {
|
|
110791
|
-
return [];
|
|
110792
|
-
}
|
|
110793
|
-
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
110794
|
-
where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
|
|
110795
|
-
});
|
|
110796
|
-
return mapEnrollmentsToUserEnrollments(enrollments, integrations);
|
|
110797
|
-
} catch (error88) {
|
|
110798
|
-
addEvent("timeback.enrollment_fetch_failed", {
|
|
110799
|
-
"exception.type": errorType(error88),
|
|
110800
|
-
"app.error.message": errorMessage(error88)
|
|
110801
|
-
});
|
|
110802
|
-
return [];
|
|
110803
|
-
}
|
|
110804
|
-
}
|
|
110805
|
-
filterOrganizationsByEnrollments(organizations, enrollments) {
|
|
110806
|
-
const enrollmentOrgIds = new Set(enrollments.map((e) => e.orgId).filter(Boolean));
|
|
110807
|
-
if (enrollmentOrgIds.size === 0) {
|
|
110808
|
-
return [];
|
|
110809
|
-
}
|
|
110810
|
-
return organizations.filter((o) => enrollmentOrgIds.has(o.id));
|
|
110811
|
-
}
|
|
110812
110775
|
}
|
|
110813
110776
|
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
110814
110777
|
var init_user_service = __esm(() => {
|
|
110815
110778
|
init_drizzle_orm();
|
|
110816
110779
|
init_src();
|
|
110817
|
-
init_helpers_index();
|
|
110818
110780
|
init_tables_index();
|
|
110819
110781
|
init_spans();
|
|
110820
110782
|
init_errors2();
|
|
110821
|
-
init_timeback_util();
|
|
110822
110783
|
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
110823
110784
|
});
|
|
110824
110785
|
|
|
@@ -169347,8 +169308,12 @@ async function buildLocalDayContext(db2, user, observedTimeZone) {
|
|
|
169347
169308
|
observedAt: null
|
|
169348
169309
|
};
|
|
169349
169310
|
}
|
|
169311
|
+
function getMockTimebackProfile(timebackId) {
|
|
169312
|
+
const { role, organizations } = getMockStudentProfile();
|
|
169313
|
+
return { id: timebackId, role, organizations };
|
|
169314
|
+
}
|
|
169350
169315
|
async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
169351
|
-
const timeback3 = user.timebackId ?
|
|
169316
|
+
const timeback3 = user.timebackId ? getMockTimebackProfile(user.timebackId) : undefined;
|
|
169352
169317
|
const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
|
|
169353
169318
|
if (gameId) {
|
|
169354
169319
|
return {
|
package/dist/mocks/timeback.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare function getMockTimebackData(db: DatabaseInstance, timebackId: st
|
|
|
32
32
|
export declare function getMockTimebackUser(db: DatabaseInstance, gameId?: string): Promise<UserTimebackData>;
|
|
33
33
|
export declare function getMockHighestGradeMastered(enrollments: UserEnrollment[], subject: string): number | null;
|
|
34
34
|
/**
|
|
35
|
-
* Build a complete user response with mock timeback
|
|
35
|
+
* Build a complete user response with mock timeback identity.
|
|
36
36
|
* Used to bypass api-core when in mock mode (avoids real API calls).
|
|
37
37
|
*/
|
|
38
38
|
export declare function buildMockUserResponse(db: DatabaseInstance, user: User, gameId?: string, observedTimeZone?: string | null): Promise<AuthenticatedUser | GameUser>;
|
package/dist/server.js
CHANGED
|
@@ -1141,7 +1141,7 @@ var package_default;
|
|
|
1141
1141
|
var init_package = __esm(() => {
|
|
1142
1142
|
package_default = {
|
|
1143
1143
|
name: "@playcademy/sandbox",
|
|
1144
|
-
version: "0.11.1-beta.
|
|
1144
|
+
version: "0.11.1-beta.2",
|
|
1145
1145
|
description: "Local development server for Playcademy game development",
|
|
1146
1146
|
type: "module",
|
|
1147
1147
|
exports: {
|
|
@@ -110366,12 +110366,13 @@ class LeaderboardService {
|
|
|
110366
110366
|
eq(gameScores.gameId, gameId),
|
|
110367
110367
|
eq(users.isAnonymous, isAnonymousUser)
|
|
110368
110368
|
];
|
|
110369
|
-
const bestScoresSubquery = db2.
|
|
110369
|
+
const bestScoresSubquery = db2.selectDistinctOn([gameScores.userId, gameScores.gameId], {
|
|
110370
110370
|
userId: gameScores.userId,
|
|
110371
110371
|
gameId: gameScores.gameId,
|
|
110372
|
-
score:
|
|
110373
|
-
achievedAt:
|
|
110374
|
-
|
|
110372
|
+
score: gameScores.score,
|
|
110373
|
+
achievedAt: gameScores.achievedAt,
|
|
110374
|
+
metadata: gameScores.metadata
|
|
110375
|
+
}).from(gameScores).innerJoin(users, eq(gameScores.userId, users.id)).where(and(...conditions2)).orderBy(gameScores.userId, gameScores.gameId, desc(gameScores.score), asc(gameScores.achievedAt)).as("best_scores");
|
|
110375
110376
|
const scores = await db2.select({
|
|
110376
110377
|
userId: users.id,
|
|
110377
110378
|
username: users.username,
|
|
@@ -110379,11 +110380,11 @@ class LeaderboardService {
|
|
|
110379
110380
|
userImage: users.image,
|
|
110380
110381
|
score: bestScoresSubquery.score,
|
|
110381
110382
|
achievedAt: bestScoresSubquery.achievedAt,
|
|
110382
|
-
metadata:
|
|
110383
|
+
metadata: bestScoresSubquery.metadata,
|
|
110383
110384
|
gameId: games.id,
|
|
110384
110385
|
gameTitle: games.displayName,
|
|
110385
110386
|
gameSlug: games.slug
|
|
110386
|
-
}).from(bestScoresSubquery).innerJoin(users, eq(bestScoresSubquery.userId, users.id)).innerJoin(games, eq(bestScoresSubquery.gameId, games.id)).
|
|
110387
|
+
}).from(bestScoresSubquery).innerJoin(users, eq(bestScoresSubquery.userId, users.id)).innerJoin(games, eq(bestScoresSubquery.gameId, games.id)).orderBy(desc(bestScoresSubquery.score), asc(bestScoresSubquery.achievedAt), asc(bestScoresSubquery.userId)).limit(limit).offset(offset);
|
|
110387
110388
|
setAttributes({
|
|
110388
110389
|
"app.leaderboard.result_count": scores.length,
|
|
110389
110390
|
"app.leaderboard.no_result": scores.length === 0
|
|
@@ -110623,7 +110624,7 @@ class UserService {
|
|
|
110623
110624
|
"app.user.timeback_enriched": false
|
|
110624
110625
|
});
|
|
110625
110626
|
const localDay = await this.resolveLocalDay(userData, observedTimeZone);
|
|
110626
|
-
const timeback2 = userData.timebackId ? await this.
|
|
110627
|
+
const timeback2 = userData.timebackId ? await this.fetchTimebackProfile(userData.timebackId) : undefined;
|
|
110627
110628
|
setAttribute("app.user.timeback_enriched", Boolean(timeback2));
|
|
110628
110629
|
if (gameId) {
|
|
110629
110630
|
return {
|
|
@@ -110728,18 +110729,10 @@ class UserService {
|
|
|
110728
110729
|
observedAt: null
|
|
110729
110730
|
};
|
|
110730
110731
|
}
|
|
110731
|
-
async
|
|
110732
|
-
const
|
|
110733
|
-
|
|
110734
|
-
|
|
110735
|
-
]);
|
|
110736
|
-
const enrollments = gameId ? filterEnrollmentsByGame(allEnrollments, gameId) : allEnrollments;
|
|
110737
|
-
const organizations = gameId ? this.filterOrganizationsByEnrollments(allOrganizations, enrollments) : allOrganizations;
|
|
110738
|
-
setAttributes({
|
|
110739
|
-
"app.timeback.enrollment_count": enrollments.length,
|
|
110740
|
-
"app.timeback.organization_count": organizations.length
|
|
110741
|
-
});
|
|
110742
|
-
return { id: timebackId, role, enrollments, organizations };
|
|
110732
|
+
async fetchTimebackProfile(timebackId) {
|
|
110733
|
+
const { role, organizations } = await withSpan("timeback.fetch_profile", () => this.fetchStudentProfile(timebackId));
|
|
110734
|
+
setAttribute("app.timeback.organization_count", organizations.length);
|
|
110735
|
+
return { id: timebackId, role, organizations };
|
|
110743
110736
|
}
|
|
110744
110737
|
async fetchStudentProfile(timebackId) {
|
|
110745
110738
|
if (!this.deps.timeback) {
|
|
@@ -110778,46 +110771,14 @@ class UserService {
|
|
|
110778
110771
|
return { role: "student", organizations: [] };
|
|
110779
110772
|
}
|
|
110780
110773
|
}
|
|
110781
|
-
async fetchEnrollments(timebackId) {
|
|
110782
|
-
if (!this.deps.timeback) {
|
|
110783
|
-
setAttribute("app.timeback.client", "unavailable");
|
|
110784
|
-
return [];
|
|
110785
|
-
}
|
|
110786
|
-
try {
|
|
110787
|
-
const enrollments = await this.deps.timeback.roster.getEnrollments(timebackId);
|
|
110788
|
-
const courseIds = enrollments.map((e) => e.courseId).filter((id) => Boolean(id));
|
|
110789
|
-
if (courseIds.length === 0) {
|
|
110790
|
-
return [];
|
|
110791
|
-
}
|
|
110792
|
-
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
110793
|
-
where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
|
|
110794
|
-
});
|
|
110795
|
-
return mapEnrollmentsToUserEnrollments(enrollments, integrations);
|
|
110796
|
-
} catch (error88) {
|
|
110797
|
-
addEvent("timeback.enrollment_fetch_failed", {
|
|
110798
|
-
"exception.type": errorType(error88),
|
|
110799
|
-
"app.error.message": errorMessage(error88)
|
|
110800
|
-
});
|
|
110801
|
-
return [];
|
|
110802
|
-
}
|
|
110803
|
-
}
|
|
110804
|
-
filterOrganizationsByEnrollments(organizations, enrollments) {
|
|
110805
|
-
const enrollmentOrgIds = new Set(enrollments.map((e) => e.orgId).filter(Boolean));
|
|
110806
|
-
if (enrollmentOrgIds.size === 0) {
|
|
110807
|
-
return [];
|
|
110808
|
-
}
|
|
110809
|
-
return organizations.filter((o) => enrollmentOrgIds.has(o.id));
|
|
110810
|
-
}
|
|
110811
110774
|
}
|
|
110812
110775
|
var LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS;
|
|
110813
110776
|
var init_user_service = __esm(() => {
|
|
110814
110777
|
init_drizzle_orm();
|
|
110815
110778
|
init_src();
|
|
110816
|
-
init_helpers_index();
|
|
110817
110779
|
init_tables_index();
|
|
110818
110780
|
init_spans();
|
|
110819
110781
|
init_errors2();
|
|
110820
|
-
init_timeback_util();
|
|
110821
110782
|
LEARNING_TIME_ZONE_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
110822
110783
|
});
|
|
110823
110784
|
|
|
@@ -169346,8 +169307,12 @@ async function buildLocalDayContext(db2, user, observedTimeZone) {
|
|
|
169346
169307
|
observedAt: null
|
|
169347
169308
|
};
|
|
169348
169309
|
}
|
|
169310
|
+
function getMockTimebackProfile(timebackId) {
|
|
169311
|
+
const { role, organizations } = getMockStudentProfile();
|
|
169312
|
+
return { id: timebackId, role, organizations };
|
|
169313
|
+
}
|
|
169349
169314
|
async function buildMockUserResponse(db2, user, gameId, observedTimeZone) {
|
|
169350
|
-
const timeback3 = user.timebackId ?
|
|
169315
|
+
const timeback3 = user.timebackId ? getMockTimebackProfile(user.timebackId) : undefined;
|
|
169351
169316
|
const localDay = await buildLocalDayContext(db2, user, observedTimeZone);
|
|
169352
169317
|
if (gameId) {
|
|
169353
169318
|
return {
|