playcademy 0.11.14 → 0.12.1

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.
Files changed (30) hide show
  1. package/dist/constants.d.ts +72 -1
  2. package/dist/constants.js +46 -0
  3. package/dist/db.d.ts +61 -0
  4. package/dist/db.js +671 -0
  5. package/dist/edge-play/src/constants.ts +3 -28
  6. package/dist/{types.d.ts → index.d.ts} +118 -14
  7. package/dist/index.js +4650 -6635
  8. package/dist/templates/api/sample-route-with-db.ts.template +141 -0
  9. package/dist/templates/config/playcademy.config.js.template +4 -0
  10. package/dist/templates/config/playcademy.config.json.template +3 -0
  11. package/dist/templates/config/timeback-config.js.template +8 -0
  12. package/dist/templates/database/db-index.ts.template +21 -0
  13. package/dist/templates/database/db-schema-index.ts.template +8 -0
  14. package/dist/templates/database/db-schema-scores.ts.template +43 -0
  15. package/dist/templates/database/db-schema-users.ts.template +23 -0
  16. package/dist/templates/database/db-seed.ts.template +52 -0
  17. package/dist/templates/database/db-types.ts.template +21 -0
  18. package/dist/templates/database/drizzle-config.ts.template +13 -0
  19. package/dist/templates/database/package.json.template +20 -0
  20. package/dist/templates/gitignore.template +17 -0
  21. package/dist/templates/playcademy-gitignore.template +3 -0
  22. package/dist/utils.d.ts +31 -14
  23. package/dist/utils.js +523 -490
  24. package/package.json +19 -3
  25. package/dist/templates/backend-config.js.template +0 -6
  26. package/dist/templates/playcademy.config.js.template +0 -4
  27. package/dist/templates/playcademy.config.json.template +0 -3
  28. package/dist/templates/timeback-config.js.template +0 -17
  29. /package/dist/templates/{sample-route.ts → api/sample-route.ts.template} +0 -0
  30. /package/dist/templates/{integrations-config.js.template → config/integrations-config.js.template} +0 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Sample API route with database
3
+ *
4
+ * This route will be available at: https://<your-game-slug>.playcademy.gg/api/hello
5
+ */
6
+
7
+ import { desc } from 'drizzle-orm'
8
+
9
+ import { getDb, schema } from '../../db'
10
+
11
+ import type { Context } from 'hono'
12
+
13
+ /**
14
+ * Request body for score submission
15
+ */
16
+ interface ScoreSubmission {
17
+ /** Score value (must be >= 0) */
18
+ score: number
19
+ /** Optional level where score was earned (defaults to 1) */
20
+ level?: number
21
+ }
22
+
23
+ /**
24
+ * GET /api/hello
25
+ *
26
+ * Retrieve recent scores with user information
27
+ */
28
+ export async function GET(c: Context): Promise<Response> {
29
+ try {
30
+ const db = getDb(c.env.DB)
31
+
32
+ const scores = await db.query.scores.findMany({
33
+ limit: 10,
34
+ orderBy: desc(schema.scores.id),
35
+ with: {
36
+ user: true,
37
+ },
38
+ })
39
+
40
+ return c.json({
41
+ success: true,
42
+ data: {
43
+ scores,
44
+ total: scores.length,
45
+ },
46
+ })
47
+ } catch (error) {
48
+ return c.json(
49
+ {
50
+ success: false,
51
+ error: 'Failed to fetch scores',
52
+ details: error instanceof Error ? error.message : String(error),
53
+ },
54
+ 500,
55
+ )
56
+ }
57
+ }
58
+
59
+ /**
60
+ * POST /api/hello
61
+ *
62
+ * Submit a new score for the current user
63
+ */
64
+ export async function POST(c: Context): Promise<Response> {
65
+ try {
66
+ const body = (await c.req.json()) as ScoreSubmission
67
+
68
+ if (!body.score || body.score < 0) {
69
+ return c.json(
70
+ {
71
+ success: false,
72
+ error: 'Invalid score value',
73
+ },
74
+ 400,
75
+ )
76
+ }
77
+
78
+ const db = getDb(c.env.DB)
79
+
80
+ // Get or create a demo user
81
+ let user = await db.select().from(schema.users).limit(1).get()
82
+
83
+ if (!user) {
84
+ // Auto-create demo user on first score submission
85
+ const [newUser] = await db
86
+ .insert(schema.users)
87
+ .values({
88
+ name: 'Demo Player',
89
+ createdAt: new Date().toISOString(),
90
+ updatedAt: new Date().toISOString(),
91
+ })
92
+ .returning()
93
+
94
+ if (!newUser) {
95
+ return c.json(
96
+ {
97
+ success: false,
98
+ error: 'Failed to create user',
99
+ },
100
+ 500,
101
+ )
102
+ }
103
+
104
+ user = newUser
105
+ }
106
+
107
+ const [newScore] = await db
108
+ .insert(schema.scores)
109
+ .values({
110
+ userId: user.id,
111
+ score: body.score,
112
+ level: body.level ?? 1,
113
+ createdAt: new Date().toISOString(),
114
+ })
115
+ .returning()
116
+
117
+ return c.json({
118
+ success: true,
119
+ data: {
120
+ score: newScore,
121
+ },
122
+ })
123
+ } catch (error) {
124
+ return c.json(
125
+ {
126
+ success: false,
127
+ error: 'Failed to save score',
128
+ details: error instanceof Error ? error.message : String(error),
129
+ },
130
+ 500,
131
+ )
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Environment variables available via c.env:
137
+ * - c.env.PLAYCADEMY_API_KEY - Game-scoped API key
138
+ * - c.env.GAME_ID - Your game's unique ID
139
+ * - c.env.PLAYCADEMY_BASE_URL - Playcademy platform URL
140
+ * - c.env.DB - D1 database binding
141
+ */
@@ -0,0 +1,4 @@
1
+ /** @type {import('playcademy/types').PlaycademyConfig} */
2
+ export default {
3
+ name: '{{GAME_NAME}}',{{GAME_DESCRIPTION}}{{GAME_EMOJI}}{{INTEGRATIONS_CONFIG}}
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "{{GAME_NAME}}"{{GAME_DESCRIPTION}}{{GAME_EMOJI}}{{INTEGRATIONS_CONFIG}}
3
+ }
@@ -0,0 +1,8 @@
1
+ // TimeBack integration configuration
2
+ // See https://docs.playcademy.com/timeback for more details
3
+ timeback: {
4
+ course: {
5
+ subjects: {{SUBJECTS}},{{DEFAULT_SUBJECT}}
6
+ grades: {{GRADES}},
7
+ },
8
+ },
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Database Client
3
+ *
4
+ * Initialize Drizzle ORM with D1 database binding.
5
+ */
6
+
7
+ import { drizzle } from 'drizzle-orm/d1'
8
+
9
+ import * as schema from './schema'
10
+
11
+ import type { D1Database } from '@cloudflare/workers-types'
12
+
13
+ /**
14
+ * Get a Drizzle client instance from a D1 database binding
15
+ */
16
+ export function getDb(d1: D1Database) {
17
+ return drizzle(d1, { schema })
18
+ }
19
+
20
+ export { schema }
21
+
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Schema Index
3
+ *
4
+ * Export all schema definitions from this file.
5
+ */
6
+
7
+ export * from './users'
8
+ export * from './scores'
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Scores Schema
3
+ *
4
+ * Define game score tables here using Drizzle ORM.
5
+ */
6
+
7
+ import { relations } from 'drizzle-orm'
8
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
9
+
10
+ import { users } from './users'
11
+
12
+ /**
13
+ * Scores table
14
+ *
15
+ * Tracks game scores per user with level information
16
+ */
17
+ export const scores = sqliteTable('scores', {
18
+ /** Unique score ID */
19
+ id: integer('id').primaryKey({ autoIncrement: true }),
20
+ /** Reference to user who earned this score */
21
+ userId: integer('user_id')
22
+ .notNull()
23
+ .references(() => users.id),
24
+ /** Score value (points earned) */
25
+ score: integer('score').notNull(),
26
+ /** Level where score was earned */
27
+ level: integer('level').notNull(),
28
+ /** Timestamp when score was created */
29
+ createdAt: text('created_at').notNull(),
30
+ })
31
+
32
+ /**
33
+ * Score relations
34
+ *
35
+ * Defines how scores relate to users (many-to-one)
36
+ */
37
+ export const scoresRelations = relations(scores, ({ one }) => ({
38
+ user: one(users, {
39
+ fields: [scores.userId],
40
+ references: [users.id],
41
+ }),
42
+ }))
43
+
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Users Schema
3
+ *
4
+ * Define user-related tables here using Drizzle ORM.
5
+ */
6
+
7
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
8
+
9
+ /**
10
+ * Users table
11
+ *
12
+ * Stores basic user information
13
+ */
14
+ export const users = sqliteTable('users', {
15
+ /** Unique user ID */
16
+ id: integer('id').primaryKey({ autoIncrement: true }),
17
+ /** User's display name */
18
+ name: text('name').notNull(),
19
+ /** Timestamp when user was created */
20
+ createdAt: text('created_at').notNull(),
21
+ /** Timestamp when user was last updated */
22
+ updatedAt: text('updated_at').notNull(),
23
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Database Seed
3
+ *
4
+ * Populate the database with initial data.
5
+ */
6
+
7
+ import Database from 'better-sqlite3'
8
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
9
+
10
+ import { getPath } from 'playcademy/db'
11
+
12
+ import * as schema from './schema'
13
+
14
+ /**
15
+ * Seed the database with initial data
16
+ */
17
+ export async function seed() {
18
+ const dbPath = getPath()
19
+ const sqlite = new Database(dbPath)
20
+ const db = drizzle(sqlite, { schema })
21
+
22
+ // Seed users
23
+ const [user] = await db
24
+ .insert(schema.users)
25
+ .values({
26
+ name: 'Demo User',
27
+ createdAt: new Date().toISOString(),
28
+ updatedAt: new Date().toISOString(),
29
+ })
30
+ .returning()
31
+
32
+ if (!user) {
33
+ throw new Error('Failed to seed user')
34
+ }
35
+
36
+ // Seed scores
37
+ await db.insert(schema.scores).values({
38
+ userId: user.id,
39
+ score: 100,
40
+ level: 1,
41
+ createdAt: new Date().toISOString(),
42
+ })
43
+
44
+ sqlite.close()
45
+ }
46
+
47
+ /**
48
+ * Run seed if this file is executed directly (from e.g. a package.json script)
49
+ */
50
+ if (import.meta.url === `file://${process.argv[1]}`) {
51
+ seed().catch(console.error)
52
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Database Types
3
+ *
4
+ * Type-safe exports inferred from Drizzle schema
5
+ */
6
+
7
+ import * as schema from './schema'
8
+
9
+ import type { InferSelectModel } from 'drizzle-orm'
10
+
11
+ /** User record from database */
12
+ export type User = InferSelectModel<typeof schema.users>
13
+
14
+ /** Score record from database */
15
+ export type Score = InferSelectModel<typeof schema.scores>
16
+
17
+ /** Score with user information populated */
18
+ export type ScoreWithUser = Score & {
19
+ user: User | null
20
+ }
21
+
@@ -0,0 +1,13 @@
1
+ import { getPath } from 'playcademy/db'
2
+
3
+ import { defineConfig } from 'drizzle-kit'
4
+
5
+ export default defineConfig({
6
+ schema: './db/schema/index.ts',
7
+ out: './db/migrations',
8
+ dialect: 'sqlite',
9
+ dbCredentials: {
10
+ url: getPath(),
11
+ },
12
+ })
13
+
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{GAME_NAME}}-backend",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "scripts": {
7
+ "db:push": "drizzle-kit push",
8
+ "db:studio": "drizzle-kit studio"
9
+ },
10
+ "dependencies": {
11
+ "drizzle-orm": "^0.42.0",
12
+ "better-sqlite3": "^12.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "drizzle-kit": "^0.30.0",
16
+ "@types/better-sqlite3": "^7.6.0",
17
+ "@cloudflare/workers-types": "^4.0.0"
18
+ }
19
+ }
20
+
@@ -0,0 +1,17 @@
1
+ # dependencies
2
+ node_modules/
3
+
4
+ # logs
5
+ logs
6
+ *.log
7
+ npm-debug.log*
8
+ yarn-debug.log*
9
+ yarn-error.log*
10
+ pnpm-debug.log*
11
+ lerna-debug.log*
12
+
13
+ # environment
14
+ .env
15
+
16
+ # os
17
+ .DS_Store
@@ -0,0 +1,3 @@
1
+ *.zip
2
+ *.pid
3
+ db
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OrganizationConfig, CourseConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
2
- import * as _hono_node_server from '@hono/node-server';
2
+ import { Miniflare } from 'miniflare';
3
3
  import * as chokidar from 'chokidar';
4
4
 
5
5
  /**
@@ -24,12 +24,31 @@ interface TimebackIntegrationConfig {
24
24
  /** Component-Resource link overrides */
25
25
  componentResource?: Partial<ComponentResourceConfig>;
26
26
  }
27
+ /**
28
+ * Custom API routes integration
29
+ */
30
+ interface CustomRoutesIntegration {
31
+ /** Directory for custom API routes (defaults to 'server/api') */
32
+ directory?: string;
33
+ }
34
+ /**
35
+ * Database integration
36
+ */
37
+ interface DatabaseIntegration {
38
+ /** Database directory (defaults to 'db') */
39
+ directory?: string;
40
+ }
27
41
  /**
28
42
  * Integrations configuration
43
+ * All backend features (database, custom routes, external services) are configured here
29
44
  */
30
45
  interface IntegrationsConfig {
31
46
  /** TimeBack integration (optional) */
32
47
  timeback?: TimebackIntegrationConfig;
48
+ /** Custom API routes (optional) */
49
+ customRoutes?: CustomRoutesIntegration | boolean;
50
+ /** Database (optional) */
51
+ database?: DatabaseIntegration | boolean;
33
52
  }
34
53
  /**
35
54
  * Unified Playcademy configuration
@@ -52,12 +71,7 @@ interface PlaycademyConfig {
52
71
  externalUrl?: string;
53
72
  /** Game platform */
54
73
  platform?: 'web' | 'unity' | 'godot';
55
- /** Backend configuration */
56
- backend?: {
57
- /** Custom API routes directory (defaults to 'api') */
58
- directory?: string;
59
- };
60
- /** External integrations */
74
+ /** Integrations (database, custom routes, external services) */
61
75
  integrations?: IntegrationsConfig;
62
76
  }
63
77
 
@@ -74,6 +88,10 @@ declare function loadConfig(configPath?: string): Promise<PlaycademyConfig>;
74
88
  */
75
89
  declare function validateConfig(config: unknown): asserts config is PlaycademyConfig;
76
90
 
91
+ /**
92
+ * Development server utilities
93
+ */
94
+
77
95
  interface DevServerOptions {
78
96
  port: number;
79
97
  config: PlaycademyConfig;
@@ -89,19 +107,18 @@ interface DevServerOptions {
89
107
  platformUrl?: string;
90
108
  }
91
109
  /**
92
- * Start the local development server
93
- * Returns the server instance which can be closed for hot reload
110
+ * Start the local development server using Miniflare
111
+ * Returns the Miniflare instance which can be disposed for hot reload
94
112
  */
95
- declare function startDevServer(options: DevServerOptions): Promise<_hono_node_server.ServerType>;
113
+ declare function startDevServer(options: DevServerOptions): Promise<Miniflare>;
96
114
 
97
- /**
98
- * Hot reload utilities for dev server
99
- */
100
115
  interface HotReloadOptions {
101
116
  /** Custom success logger (defaults to CLI logger) */
102
- onSuccess?: (changedPath?: string) => void;
117
+ onSuccess?: (changedPath?: string, eventType?: string) => void;
103
118
  /** Custom error logger (defaults to CLI logger) */
104
119
  onError?: (error: unknown) => void;
120
+ /** Playcademy config (to determine backend directory) */
121
+ config?: PlaycademyConfig | null;
105
122
  }
106
123
  /**
107
124
  * Start watching files for changes and reload server