create-fullstack-scaffold 0.6.0 → 0.6.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/package.json +1 -1
- package/template/CLAUDE.md +3 -3
- package/template/PROJECT_SUMMARY.md +1 -1
- package/template/QUICKSTART.md +3 -3
- package/template/README.md +2 -2
- package/template/drizzle/0000_init_full_schema.sql +390 -0
- package/template/drizzle/meta/0000_snapshot.json +2487 -0
- package/template/drizzle/meta/_journal.json +13 -0
- package/template/package.json +1 -1
- package/template/src/server/db/driver.ts +118 -69
package/template/package.json
CHANGED
|
@@ -1,116 +1,165 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from 'fs'
|
|
2
|
-
import { dirname } from 'path'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { drizzle as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
2
|
+
import { dirname } from 'path'
|
|
3
|
+
import { createHash } from 'crypto'
|
|
4
|
+
import { getDatabaseConfig, type DatabaseConfig } from '../config'
|
|
5
|
+
import * as schema from './schema'
|
|
6
|
+
import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'
|
|
7
|
+
import { drizzle as drizzleD1 } from 'drizzle-orm/d1'
|
|
8
|
+
import { createClient, type Client } from '@libsql/client'
|
|
9
|
+
import { logger } from '../utils/logger'
|
|
10
|
+
import { isCloudflare } from '../utils/env'
|
|
10
11
|
|
|
11
|
-
type LibSQLDb = ReturnType<typeof drizzleLibsql<typeof schema
|
|
12
|
-
type D1Db = ReturnType<typeof drizzleD1<typeof schema
|
|
13
|
-
type Db = LibSQLDb | D1Db
|
|
12
|
+
type LibSQLDb = ReturnType<typeof drizzleLibsql<typeof schema>>
|
|
13
|
+
type D1Db = ReturnType<typeof drizzleD1<typeof schema>>
|
|
14
|
+
type Db = LibSQLDb | D1Db
|
|
14
15
|
|
|
15
|
-
let _db: Db | null = null
|
|
16
|
-
let _client: Client | null = null
|
|
16
|
+
let _db: Db | null = null
|
|
17
|
+
let _client: Client | null = null
|
|
17
18
|
|
|
18
|
-
const log = logger.db()
|
|
19
|
+
const log = logger.db()
|
|
19
20
|
|
|
20
21
|
export async function getDb(): Promise<Db> {
|
|
21
|
-
if (_db) return _db
|
|
22
|
-
|
|
23
|
-
const config = getDatabaseConfig()
|
|
24
|
-
|
|
25
|
-
log.debug({ driver: config.driver }, 'Creating database connection')
|
|
26
|
-
|
|
22
|
+
if (_db) return _db
|
|
23
|
+
|
|
24
|
+
const config = getDatabaseConfig()
|
|
25
|
+
|
|
26
|
+
log.debug({ driver: config.driver }, 'Creating database connection')
|
|
27
|
+
|
|
27
28
|
if (config.driver === 'd1') {
|
|
28
|
-
_db = createD1Db(config)
|
|
29
|
+
_db = createD1Db(config)
|
|
29
30
|
} else {
|
|
30
|
-
const result = createSqliteDb(config)
|
|
31
|
-
_db = result.db
|
|
32
|
-
_client = result.client
|
|
31
|
+
const result = createSqliteDb(config)
|
|
32
|
+
_db = result.db
|
|
33
|
+
_client = result.client
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
log.info({ driver: config.driver }, 'Database connected')
|
|
36
|
-
return _db
|
|
35
|
+
|
|
36
|
+
log.info({ driver: config.driver }, 'Database connected')
|
|
37
|
+
return _db
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function createSqliteDb(config: DatabaseConfig): { db: LibSQLDb; client: Client } {
|
|
40
41
|
if (isCloudflare) {
|
|
41
|
-
throw new Error('SQLite is not supported in Cloudflare Workers. Use D1 instead.')
|
|
42
|
+
throw new Error('SQLite is not supported in Cloudflare Workers. Use D1 instead.')
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
const dbPath = config.sqlitePath || './data/app.db'
|
|
45
|
-
|
|
44
|
+
|
|
45
|
+
const dbPath = config.sqlitePath || './data/app.db'
|
|
46
|
+
|
|
46
47
|
if (dbPath !== ':memory:') {
|
|
47
|
-
const dbDir = dirname(dbPath)
|
|
48
|
+
const dbDir = dirname(dbPath)
|
|
48
49
|
if (!existsSync(dbDir)) {
|
|
49
|
-
mkdirSync(dbDir, { recursive: true })
|
|
50
|
-
log.debug({ dir: dbDir }, 'Created database directory')
|
|
50
|
+
mkdirSync(dbDir, { recursive: true })
|
|
51
|
+
log.debug({ dir: dbDir }, 'Created database directory')
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
const client = createClient({ url: dbPath === ':memory:' ? ':memory:' : `file:${dbPath}` })
|
|
55
|
-
const db = drizzleLibsql(client, { schema })
|
|
56
|
-
|
|
57
|
-
log.debug({ path: dbPath }, 'SQLite database created')
|
|
58
|
-
return { db, client }
|
|
54
|
+
|
|
55
|
+
const client = createClient({ url: dbPath === ':memory:' ? ':memory:' : `file:${dbPath}` })
|
|
56
|
+
const db = drizzleLibsql(client, { schema })
|
|
57
|
+
|
|
58
|
+
log.debug({ path: dbPath }, 'SQLite database created')
|
|
59
|
+
return { db, client }
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function createD1Db(config: DatabaseConfig): D1Db {
|
|
62
63
|
if (!config.d1Database) {
|
|
63
|
-
throw new Error('D1 database binding not found. Make sure D1 is configured in wrangler.toml')
|
|
64
|
+
throw new Error('D1 database binding not found. Make sure D1 is configured in wrangler.toml')
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
return drizzleD1(config.d1Database, { schema })
|
|
66
|
+
|
|
67
|
+
return drizzleD1(config.d1Database, { schema })
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export async function getRawClient(): Promise<Client | D1Database | null> {
|
|
70
|
-
const config = getDatabaseConfig()
|
|
71
|
-
|
|
71
|
+
const config = getDatabaseConfig()
|
|
72
|
+
|
|
72
73
|
if (config.driver === 'sqlite') {
|
|
73
74
|
if (!_client) {
|
|
74
|
-
await getDb()
|
|
75
|
+
await getDb()
|
|
75
76
|
}
|
|
76
|
-
return _client
|
|
77
|
+
return _client
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
if (config.driver === 'd1') {
|
|
80
|
-
return config.d1Database || null
|
|
81
|
+
return config.d1Database || null
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
-
return null
|
|
83
|
+
|
|
84
|
+
return null
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export async function closeDb(): Promise<void> {
|
|
87
|
-
const config = getDatabaseConfig()
|
|
88
|
-
|
|
88
|
+
const config = getDatabaseConfig()
|
|
89
|
+
|
|
89
90
|
if (config.driver === 'sqlite' && _client) {
|
|
90
|
-
_client.close()
|
|
91
|
-
_client = null
|
|
92
|
-
_db = null
|
|
93
|
-
log.info({}, 'Database connection closed')
|
|
91
|
+
_client.close()
|
|
92
|
+
_client = null
|
|
93
|
+
_db = null
|
|
94
|
+
log.info({}, 'Database connection closed')
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 账本补齐:db:push 建出的库有表但没有 __drizzle_migrations 记录,
|
|
100
|
+
* migrate 会重放 0000 的 CREATE TABLE 撞表崩溃(部署流水线先 push 后
|
|
101
|
+
* start 即此形态)。检测到该历史时,把已带当前 schema 的库按"迁移已
|
|
102
|
+
* 应用"补记账本(hash=SQL 文件 sha256、created_at=journal.when,与
|
|
103
|
+
* drizzle migrator 自身写入格式一致),migrate 随后即成 no-op。
|
|
104
|
+
*/
|
|
105
|
+
async function stampJournalIfPushBuilt(migrationsFolder: string): Promise<void> {
|
|
106
|
+
if (!_client || !('execute' in _client)) return
|
|
107
|
+
|
|
108
|
+
// 核心表 + 租户表都在(= 当前 schema 形态、由 push 建成)
|
|
109
|
+
const shape = await _client.execute(
|
|
110
|
+
"SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name IN ('todos','tenants')"
|
|
111
|
+
)
|
|
112
|
+
if (Number(shape.rows[0]?.c ?? 0) < 2) return
|
|
113
|
+
|
|
114
|
+
const journalTable = await _client.execute(
|
|
115
|
+
"SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'"
|
|
116
|
+
)
|
|
117
|
+
if (Number(journalTable.rows[0]?.c ?? 0) > 0) {
|
|
118
|
+
const applied = await _client.execute('SELECT COUNT(*) AS c FROM __drizzle_migrations')
|
|
119
|
+
if (Number(applied.rows[0]?.c ?? 0) > 0) return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const metaPath = `${migrationsFolder}/meta/_journal.json`
|
|
123
|
+
if (!existsSync(metaPath)) return
|
|
124
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8')) as {
|
|
125
|
+
entries: Array<{ tag: string; when: number }>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await _client.execute(
|
|
129
|
+
'CREATE TABLE IF NOT EXISTS __drizzle_migrations (id SERIAL PRIMARY KEY, hash text NOT NULL, created_at numeric)'
|
|
130
|
+
)
|
|
131
|
+
for (const entry of meta.entries) {
|
|
132
|
+
const sqlText = readFileSync(`${migrationsFolder}/${entry.tag}.sql`, 'utf-8')
|
|
133
|
+
const hash = createHash('sha256').update(sqlText).digest('hex')
|
|
134
|
+
await _client.execute({
|
|
135
|
+
sql: 'INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)',
|
|
136
|
+
args: [hash, entry.when],
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
log.warn(
|
|
140
|
+
{ entries: meta.entries.length },
|
|
141
|
+
'Push-built database detected — migration journal stamped as applied (schema already current)'
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
97
145
|
export async function runMigrations(): Promise<void> {
|
|
98
146
|
if (isCloudflare) {
|
|
99
|
-
log.info({}, 'Migrations skipped in Cloudflare Workers')
|
|
100
|
-
return
|
|
147
|
+
log.info({}, 'Migrations skipped in Cloudflare Workers')
|
|
148
|
+
return
|
|
101
149
|
}
|
|
102
|
-
|
|
103
|
-
const config = getDatabaseConfig()
|
|
104
|
-
|
|
150
|
+
|
|
151
|
+
const config = getDatabaseConfig()
|
|
152
|
+
|
|
105
153
|
if (config.driver === 'sqlite' && _db) {
|
|
106
|
-
const migrationsFolder = './drizzle'
|
|
107
|
-
|
|
154
|
+
const migrationsFolder = './drizzle'
|
|
155
|
+
|
|
108
156
|
if (existsSync(migrationsFolder)) {
|
|
109
|
-
const { migrate } = await import('drizzle-orm/libsql/migrator')
|
|
110
|
-
await
|
|
111
|
-
|
|
157
|
+
const { migrate } = await import('drizzle-orm/libsql/migrator')
|
|
158
|
+
await stampJournalIfPushBuilt(migrationsFolder)
|
|
159
|
+
await migrate(_db as LibSQLDb, { migrationsFolder })
|
|
160
|
+
log.info({ folder: migrationsFolder }, 'Migrations applied')
|
|
112
161
|
} else {
|
|
113
|
-
log.warn({ folder: migrationsFolder }, 'No migrations folder found')
|
|
162
|
+
log.warn({ folder: migrationsFolder }, 'No migrations folder found')
|
|
114
163
|
}
|
|
115
164
|
}
|
|
116
165
|
}
|