create-fullstack-scaffold 0.6.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fullstack-scaffold": "dist/cli/index.js"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biomimic-todo-app",
3
3
  "private": true,
4
- "version": "0.6.1",
4
+ "version": "0.6.2",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "biomimic": "./dist/cli/index.js"
@@ -1,116 +1,165 @@
1
- import { existsSync, mkdirSync } from 'fs';
2
- import { dirname } from 'path';
3
- import { getDatabaseConfig, type DatabaseConfig } from '../config';
4
- import * as schema from './schema';
5
- import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
6
- import { drizzle as drizzleD1 } from 'drizzle-orm/d1';
7
- import { createClient, type Client } from '@libsql/client';
8
- import { logger } from '../utils/logger';
9
- import { isCloudflare } from '../utils/env';
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 migrate(_db as LibSQLDb, { migrationsFolder });
111
- log.info({ folder: migrationsFolder }, 'Migrations applied');
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
  }