playcademy 0.11.14 → 0.12.0
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/constants.d.ts +72 -1
- package/dist/constants.js +46 -0
- package/dist/db.d.ts +61 -0
- package/dist/db.js +671 -0
- package/dist/edge-play/src/constants.ts +3 -28
- package/dist/{types.d.ts → index.d.ts} +118 -14
- package/dist/index.js +4650 -6635
- package/dist/templates/api/sample-route-with-db.ts.template +141 -0
- package/dist/templates/config/playcademy.config.js.template +4 -0
- package/dist/templates/config/playcademy.config.json.template +3 -0
- package/dist/templates/config/timeback-config.js.template +8 -0
- package/dist/templates/database/db-index.ts.template +21 -0
- package/dist/templates/database/db-schema-index.ts.template +8 -0
- package/dist/templates/database/db-schema-scores.ts.template +43 -0
- package/dist/templates/database/db-schema-users.ts.template +23 -0
- package/dist/templates/database/db-seed.ts.template +52 -0
- package/dist/templates/database/db-types.ts.template +21 -0
- package/dist/templates/database/drizzle-config.ts.template +13 -0
- package/dist/templates/database/package.json.template +20 -0
- package/dist/templates/gitignore.template +17 -0
- package/dist/templates/playcademy-gitignore.template +3 -0
- package/dist/utils.d.ts +31 -14
- package/dist/utils.js +523 -490
- package/package.json +19 -3
- package/dist/templates/backend-config.js.template +0 -6
- package/dist/templates/playcademy.config.js.template +0 -4
- package/dist/templates/playcademy.config.json.template +0 -3
- package/dist/templates/timeback-config.js.template +0 -17
- /package/dist/templates/{sample-route.ts → api/sample-route.ts.template} +0 -0
- /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,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,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,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
|
+
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OrganizationConfig, CourseConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
|
|
2
|
-
import
|
|
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
|
-
/**
|
|
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
|
|
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<
|
|
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
|