@risingwave/wavelet-cli 0.2.1 → 0.2.5
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 +18 -5
- package/src/codegen.ts +0 -214
- package/src/config-loader.ts +0 -69
- package/src/index.ts +0 -233
- package/src/risingwave-launcher.ts +0 -177
- package/tsconfig.json +0 -8
package/package.json
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@risingwave/wavelet-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Wavelet CLI - manage views, generate types, run dev server",
|
|
5
|
+
"homepage": "https://github.com/risingwavelabs/wavelet",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/risingwavelabs/wavelet.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/risingwavelabs/wavelet/issues"
|
|
13
|
+
},
|
|
5
14
|
"bin": {
|
|
6
|
-
"wavelet": "
|
|
15
|
+
"wavelet": "dist/index.js"
|
|
7
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
8
20
|
"scripts": {
|
|
9
21
|
"build": "tsc",
|
|
10
|
-
"dev": "tsx src/index.ts"
|
|
22
|
+
"dev": "tsx src/index.ts",
|
|
23
|
+
"prepack": "npm --prefix ../config run build && npm --prefix ../server run build && npm run build"
|
|
11
24
|
},
|
|
12
25
|
"dependencies": {
|
|
13
|
-
"@risingwave/wavelet": "0.2.
|
|
14
|
-
"@risingwave/wavelet-server": "0.2.
|
|
26
|
+
"@risingwave/wavelet": "0.2.5",
|
|
27
|
+
"@risingwave/wavelet-server": "0.2.5",
|
|
15
28
|
"pg": "^8.13.0",
|
|
16
29
|
"tsx": "^4.0.0"
|
|
17
30
|
},
|
package/src/codegen.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
2
|
-
import pg from 'pg'
|
|
3
|
-
import type { WaveletConfig, SqlFragment, QueryDef, ColumnType } from '@risingwave/wavelet'
|
|
4
|
-
|
|
5
|
-
const { Client } = pg
|
|
6
|
-
|
|
7
|
-
export async function generateClient(config: WaveletConfig): Promise<void> {
|
|
8
|
-
// Event types are always known from config
|
|
9
|
-
const eventTypes: TypedEntity[] = []
|
|
10
|
-
const events = config.events ?? config.streams ?? {}
|
|
11
|
-
for (const [eventName, eventDef] of Object.entries(events)) {
|
|
12
|
-
const columns = Object.entries(eventDef.columns).map(([name, type]) => ({
|
|
13
|
-
name,
|
|
14
|
-
tsType: columnTypeToTs(type),
|
|
15
|
-
}))
|
|
16
|
-
eventTypes.push({ name: eventName, columns })
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Query types: try config-declared columns first, then RisingWave introspection, then generic
|
|
20
|
-
const queryTypes: TypedEntity[] = []
|
|
21
|
-
let dbClient: InstanceType<typeof Client> | null = null
|
|
22
|
-
const queries = config.queries ?? config.views ?? {}
|
|
23
|
-
|
|
24
|
-
for (const [queryName, queryDef] of Object.entries(queries)) {
|
|
25
|
-
// Tier 1: explicit columns in config
|
|
26
|
-
const declaredColumns = getQueryColumns(queryDef)
|
|
27
|
-
if (declaredColumns) {
|
|
28
|
-
queryTypes.push({ name: queryName, columns: declaredColumns })
|
|
29
|
-
continue
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Tier 2: introspect from RisingWave (if reachable)
|
|
33
|
-
if (!dbClient) {
|
|
34
|
-
dbClient = await tryConnect(config.database)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (dbClient) {
|
|
38
|
-
try {
|
|
39
|
-
const result = await dbClient.query(
|
|
40
|
-
`SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position`,
|
|
41
|
-
[queryName]
|
|
42
|
-
)
|
|
43
|
-
if (result.rows.length > 0) {
|
|
44
|
-
queryTypes.push({
|
|
45
|
-
name: queryName,
|
|
46
|
-
columns: result.rows.map((row: any) => ({
|
|
47
|
-
name: row.column_name,
|
|
48
|
-
tsType: pgTypeToTs(row.data_type),
|
|
49
|
-
})),
|
|
50
|
-
})
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
// Fall through to tier 3
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Tier 3: generic fallback
|
|
59
|
-
queryTypes.push({ name: queryName, columns: [] })
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (dbClient) {
|
|
63
|
-
await dbClient.end().catch(() => {})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const code = generateCode(queryTypes, eventTypes)
|
|
67
|
-
|
|
68
|
-
mkdirSync('.wavelet', { recursive: true })
|
|
69
|
-
writeFileSync('.wavelet/client.ts', code)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface TypedEntity {
|
|
73
|
-
name: string
|
|
74
|
-
columns: { name: string; tsType: string }[]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getQueryColumns(queryDef: QueryDef | SqlFragment): { name: string; tsType: string }[] | null {
|
|
78
|
-
// SqlFragment has no columns
|
|
79
|
-
if ('_tag' in queryDef && queryDef._tag === 'sql') return null
|
|
80
|
-
|
|
81
|
-
// QueryDef might have explicit columns
|
|
82
|
-
const qd = queryDef as QueryDef
|
|
83
|
-
if (!qd.columns) return null
|
|
84
|
-
|
|
85
|
-
return Object.entries(qd.columns).map(([name, type]) => ({
|
|
86
|
-
name,
|
|
87
|
-
tsType: columnTypeToTs(type),
|
|
88
|
-
}))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function tryConnect(connectionString: string): Promise<InstanceType<typeof Client> | null> {
|
|
92
|
-
const client = new Client({ connectionString, connectionTimeoutMillis: 3000 })
|
|
93
|
-
try {
|
|
94
|
-
await client.connect()
|
|
95
|
-
return client
|
|
96
|
-
} catch {
|
|
97
|
-
return null
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function generateCode(queries: TypedEntity[], events: TypedEntity[]): string {
|
|
102
|
-
let code = `// Auto-generated by wavelet generate - do not edit\n`
|
|
103
|
-
code += `// Run 'npx wavelet generate' to regenerate\n\n`
|
|
104
|
-
code += `import { WaveletClient } from '@risingwave/wavelet-sdk'\n`
|
|
105
|
-
code += `import type { QueryHandle, EventHandle, Diff, WaveletClientOptions } from '@risingwave/wavelet-sdk'\n\n`
|
|
106
|
-
|
|
107
|
-
// Query row types
|
|
108
|
-
for (const q of queries) {
|
|
109
|
-
const typeName = pascalCase(q.name) + 'Row'
|
|
110
|
-
if (q.columns.length > 0) {
|
|
111
|
-
code += `export interface ${typeName} {\n`
|
|
112
|
-
for (const col of q.columns) {
|
|
113
|
-
code += ` ${col.name}: ${col.tsType}\n`
|
|
114
|
-
}
|
|
115
|
-
code += `}\n\n`
|
|
116
|
-
} else {
|
|
117
|
-
code += `export type ${typeName} = Record<string, unknown>\n\n`
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Event types
|
|
122
|
-
for (const ev of events) {
|
|
123
|
-
const typeName = pascalCase(ev.name) + 'Event'
|
|
124
|
-
code += `export interface ${typeName} {\n`
|
|
125
|
-
for (const col of ev.columns) {
|
|
126
|
-
code += ` ${col.name}: ${col.tsType}\n`
|
|
127
|
-
}
|
|
128
|
-
code += `}\n\n`
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Typed client class
|
|
132
|
-
code += `export class TypedWaveletClient {\n`
|
|
133
|
-
code += ` private client: WaveletClient\n\n`
|
|
134
|
-
code += ` constructor(options: WaveletClientOptions) {\n`
|
|
135
|
-
code += ` this.client = new WaveletClient(options)\n`
|
|
136
|
-
code += ` }\n\n`
|
|
137
|
-
|
|
138
|
-
// Query accessors
|
|
139
|
-
code += ` queries = {\n`
|
|
140
|
-
for (const q of queries) {
|
|
141
|
-
const typeName = pascalCase(q.name) + 'Row'
|
|
142
|
-
code += ` ${q.name}: this.client.query<${typeName}>('${q.name}'),\n`
|
|
143
|
-
}
|
|
144
|
-
code += ` }\n\n`
|
|
145
|
-
|
|
146
|
-
// Event accessors
|
|
147
|
-
code += ` events = {\n`
|
|
148
|
-
for (const ev of events) {
|
|
149
|
-
const typeName = pascalCase(ev.name) + 'Event'
|
|
150
|
-
code += ` ${ev.name}: this.client.event<${typeName}>('${ev.name}'),\n`
|
|
151
|
-
}
|
|
152
|
-
code += ` }\n`
|
|
153
|
-
code += `}\n\n`
|
|
154
|
-
|
|
155
|
-
// Query name literal type
|
|
156
|
-
const queryNames = queries.map(q => `'${q.name}'`).join(' | ')
|
|
157
|
-
code += `export type QueryName = ${queryNames || 'never'}\n\n`
|
|
158
|
-
|
|
159
|
-
// React hook overloads
|
|
160
|
-
code += `// React hook overloads for type-safe useWavelet\n`
|
|
161
|
-
code += `import type { UseWaveletResult } from '@risingwave/wavelet-sdk/react'\n\n`
|
|
162
|
-
for (const q of queries) {
|
|
163
|
-
const typeName = pascalCase(q.name) + 'Row'
|
|
164
|
-
code += `export declare function useWavelet(query: '${q.name}'): UseWaveletResult<${typeName}>\n`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return code
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function pgTypeToTs(pgType: string): string {
|
|
171
|
-
switch (pgType.toLowerCase()) {
|
|
172
|
-
case 'integer':
|
|
173
|
-
case 'bigint':
|
|
174
|
-
case 'smallint':
|
|
175
|
-
case 'real':
|
|
176
|
-
case 'double precision':
|
|
177
|
-
case 'numeric':
|
|
178
|
-
case 'float':
|
|
179
|
-
case 'int':
|
|
180
|
-
return 'number'
|
|
181
|
-
case 'boolean':
|
|
182
|
-
return 'boolean'
|
|
183
|
-
case 'jsonb':
|
|
184
|
-
case 'json':
|
|
185
|
-
return 'unknown'
|
|
186
|
-
case 'timestamp with time zone':
|
|
187
|
-
case 'timestamp without time zone':
|
|
188
|
-
case 'timestamptz':
|
|
189
|
-
return 'string'
|
|
190
|
-
default:
|
|
191
|
-
return 'string'
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function columnTypeToTs(colType: string): string {
|
|
196
|
-
switch (colType) {
|
|
197
|
-
case 'int':
|
|
198
|
-
case 'float':
|
|
199
|
-
return 'number'
|
|
200
|
-
case 'boolean':
|
|
201
|
-
return 'boolean'
|
|
202
|
-
case 'json':
|
|
203
|
-
return 'unknown'
|
|
204
|
-
default:
|
|
205
|
-
return 'string'
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function pascalCase(str: string): string {
|
|
210
|
-
return str
|
|
211
|
-
.split(/[_\s-]+/)
|
|
212
|
-
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
213
|
-
.join('')
|
|
214
|
-
}
|
package/src/config-loader.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import type { WaveletConfig } from '@risingwave/wavelet'
|
|
4
|
-
|
|
5
|
-
export async function loadConfig(configPath: string): Promise<WaveletConfig> {
|
|
6
|
-
const abs = resolve(configPath)
|
|
7
|
-
|
|
8
|
-
// For .ts files, use tsx to evaluate the config
|
|
9
|
-
if (abs.endsWith('.ts')) {
|
|
10
|
-
return loadTsConfig(abs)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// For .js/.mjs files, use dynamic import directly
|
|
14
|
-
const { pathToFileURL } = await import('node:url')
|
|
15
|
-
const mod = await import(pathToFileURL(abs).href)
|
|
16
|
-
return unwrapConfig(mod)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function loadTsConfig(absPath: string): WaveletConfig {
|
|
20
|
-
// Use tsx to evaluate the TypeScript config and extract the result as JSON
|
|
21
|
-
const script = `
|
|
22
|
-
import('${absPath}').then(mod => {
|
|
23
|
-
let config = mod.default ?? mod;
|
|
24
|
-
if (config && typeof config === 'object' && 'default' in config) config = config.default;
|
|
25
|
-
process.stdout.write(JSON.stringify(config));
|
|
26
|
-
}).catch(err => {
|
|
27
|
-
process.stderr.write(err.message);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
});
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const result = execSync(`npx tsx -e "${script.replace(/"/g, '\\"')}"`, {
|
|
34
|
-
cwd: resolve(absPath, '..'),
|
|
35
|
-
encoding: 'utf-8',
|
|
36
|
-
timeout: 15000,
|
|
37
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
-
})
|
|
39
|
-
const config = JSON.parse(result)
|
|
40
|
-
if (!config || !config.database) {
|
|
41
|
-
throw new Error('Config missing database field')
|
|
42
|
-
}
|
|
43
|
-
return config
|
|
44
|
-
} catch (err: any) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Failed to load ${absPath}.\n` +
|
|
47
|
-
`Make sure tsx is installed: npm install tsx\n` +
|
|
48
|
-
`Error: ${err.stderr || err.message}`
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function unwrapConfig(mod: any): WaveletConfig {
|
|
54
|
-
let config = mod.default ?? mod
|
|
55
|
-
if (config && typeof config === 'object' && 'default' in config) {
|
|
56
|
-
config = config.default
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!config || !config.database) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
`wavelet.config.ts must export a config with a 'database' field.\n` +
|
|
62
|
-
`Example:\n` +
|
|
63
|
-
` import { defineConfig } from '@risingwave/wavelet'\n` +
|
|
64
|
-
` export default defineConfig({ database: 'postgres://...' })`
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return config
|
|
69
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const command = process.argv[2]
|
|
4
|
-
|
|
5
|
-
const HELP = `
|
|
6
|
-
wavelet - Subscribe to computed results, not raw rows.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
wavelet <command> [options]
|
|
10
|
-
|
|
11
|
-
Commands:
|
|
12
|
-
dev Start local development server
|
|
13
|
-
generate Generate typed client from query definitions
|
|
14
|
-
push Sync query definitions to Wavelet server
|
|
15
|
-
status Show current configuration and connection status
|
|
16
|
-
init Initialize a new Wavelet project
|
|
17
|
-
|
|
18
|
-
Options:
|
|
19
|
-
--config Path to wavelet.config.ts (default: ./wavelet.config.ts)
|
|
20
|
-
--json Output in JSON format
|
|
21
|
-
--help Show this help message
|
|
22
|
-
|
|
23
|
-
Examples:
|
|
24
|
-
wavelet init
|
|
25
|
-
wavelet dev
|
|
26
|
-
wavelet generate
|
|
27
|
-
wavelet push
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
async function main() {
|
|
31
|
-
switch (command) {
|
|
32
|
-
case 'init':
|
|
33
|
-
await runInit()
|
|
34
|
-
break
|
|
35
|
-
case 'dev':
|
|
36
|
-
await runDev()
|
|
37
|
-
break
|
|
38
|
-
case 'generate':
|
|
39
|
-
await runGenerate()
|
|
40
|
-
break
|
|
41
|
-
case 'push':
|
|
42
|
-
await runPush()
|
|
43
|
-
break
|
|
44
|
-
case 'status':
|
|
45
|
-
await runStatus()
|
|
46
|
-
break
|
|
47
|
-
case '--help':
|
|
48
|
-
case '-h':
|
|
49
|
-
case undefined:
|
|
50
|
-
console.log(HELP)
|
|
51
|
-
break
|
|
52
|
-
default:
|
|
53
|
-
console.error(`Unknown command: '${command}'`)
|
|
54
|
-
console.error(`Run 'wavelet --help' for available commands.`)
|
|
55
|
-
process.exit(1)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function runInit() {
|
|
60
|
-
const { writeFileSync, existsSync } = await import('node:fs')
|
|
61
|
-
|
|
62
|
-
if (existsSync('wavelet.config.ts')) {
|
|
63
|
-
console.log('wavelet.config.ts already exists. Skipping.')
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
writeFileSync('wavelet.config.ts', `import { defineConfig, sql } from '@risingwave/wavelet'
|
|
68
|
-
|
|
69
|
-
export default defineConfig({
|
|
70
|
-
database: process.env.WAVELET_DATABASE_URL ?? 'postgres://root@localhost:4566/dev',
|
|
71
|
-
|
|
72
|
-
events: {
|
|
73
|
-
// Define your events here
|
|
74
|
-
// game_events: {
|
|
75
|
-
// columns: {
|
|
76
|
-
// user_id: 'string',
|
|
77
|
-
// action: 'string',
|
|
78
|
-
// value: 'int',
|
|
79
|
-
// }
|
|
80
|
-
// }
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
queries: {
|
|
84
|
-
// Define your queries (materialized views) here
|
|
85
|
-
// leaderboard: sql\`
|
|
86
|
-
// SELECT user_id, SUM(value) as total
|
|
87
|
-
// FROM game_events
|
|
88
|
-
// GROUP BY user_id
|
|
89
|
-
// ORDER BY total DESC
|
|
90
|
-
// LIMIT 100
|
|
91
|
-
// \`,
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
`)
|
|
95
|
-
|
|
96
|
-
console.log('Created wavelet.config.ts')
|
|
97
|
-
console.log('')
|
|
98
|
-
console.log('Next steps:')
|
|
99
|
-
console.log(' 1. Edit wavelet.config.ts to define your events and queries')
|
|
100
|
-
console.log(' 2. Run: wavelet dev')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function runDev() {
|
|
104
|
-
const { loadConfig } = await import('./config-loader.js')
|
|
105
|
-
const { ensureRisingWave } = await import('./risingwave-launcher.js')
|
|
106
|
-
const configPath = getConfigPath()
|
|
107
|
-
|
|
108
|
-
console.log(`Loading config from ${configPath}...`)
|
|
109
|
-
const config = await loadConfig(configPath)
|
|
110
|
-
|
|
111
|
-
// Ensure RisingWave is running
|
|
112
|
-
const rwProcess = await ensureRisingWave(config.database)
|
|
113
|
-
|
|
114
|
-
// Sync DDL before starting server
|
|
115
|
-
const { DdlManager, WaveletServer } = await import('@risingwave/wavelet-server')
|
|
116
|
-
const ddl = new DdlManager(config.database)
|
|
117
|
-
await ddl.connect()
|
|
118
|
-
|
|
119
|
-
console.log('\nSyncing events and queries...')
|
|
120
|
-
const actions = await ddl.sync(config)
|
|
121
|
-
printDdlActions(actions)
|
|
122
|
-
await ddl.close()
|
|
123
|
-
|
|
124
|
-
// Start server
|
|
125
|
-
const server = new WaveletServer(config)
|
|
126
|
-
await server.start()
|
|
127
|
-
|
|
128
|
-
const shutdown = async () => {
|
|
129
|
-
console.log('\nShutting down...')
|
|
130
|
-
await server.stop()
|
|
131
|
-
if (rwProcess) {
|
|
132
|
-
console.log('Stopping RisingWave...')
|
|
133
|
-
rwProcess.kill()
|
|
134
|
-
}
|
|
135
|
-
process.exit(0)
|
|
136
|
-
}
|
|
137
|
-
process.on('SIGINT', shutdown)
|
|
138
|
-
process.on('SIGTERM', shutdown)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function runGenerate() {
|
|
142
|
-
const { loadConfig } = await import('./config-loader.js')
|
|
143
|
-
const { generateClient } = await import('./codegen.js')
|
|
144
|
-
const configPath = getConfigPath()
|
|
145
|
-
|
|
146
|
-
console.log(`Loading config from ${configPath}...`)
|
|
147
|
-
const config = await loadConfig(configPath)
|
|
148
|
-
|
|
149
|
-
await generateClient(config)
|
|
150
|
-
console.log('Generated .wavelet/client.ts')
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function runPush() {
|
|
154
|
-
const { loadConfig } = await import('./config-loader.js')
|
|
155
|
-
const configPath = getConfigPath()
|
|
156
|
-
|
|
157
|
-
console.log(`Loading config from ${configPath}...`)
|
|
158
|
-
const config = await loadConfig(configPath)
|
|
159
|
-
|
|
160
|
-
const { DdlManager } = await import('@risingwave/wavelet-server')
|
|
161
|
-
const ddl = new DdlManager(config.database)
|
|
162
|
-
await ddl.connect()
|
|
163
|
-
|
|
164
|
-
const actions = await ddl.sync(config)
|
|
165
|
-
await ddl.close()
|
|
166
|
-
|
|
167
|
-
const isJson = process.argv.includes('--json')
|
|
168
|
-
if (isJson) {
|
|
169
|
-
console.log(JSON.stringify({ actions }))
|
|
170
|
-
} else {
|
|
171
|
-
printDdlActions(actions)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function runStatus() {
|
|
176
|
-
const { loadConfig } = await import('./config-loader.js')
|
|
177
|
-
const configPath = getConfigPath()
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const config = await loadConfig(configPath)
|
|
181
|
-
const eventCount = Object.keys(config.events ?? config.streams ?? {}).length
|
|
182
|
-
const queryCount = Object.keys(config.queries ?? config.views ?? {}).length
|
|
183
|
-
|
|
184
|
-
console.log(`Config: ${configPath}`)
|
|
185
|
-
console.log(`Database: ${config.database.replace(/\/\/[^@]+@/, '//***@')}`)
|
|
186
|
-
console.log(`Events: ${eventCount}`)
|
|
187
|
-
console.log(`Queries: ${queryCount}`)
|
|
188
|
-
|
|
189
|
-
if (queryCount > 0) {
|
|
190
|
-
console.log('\nQueries:')
|
|
191
|
-
for (const name of Object.keys(config.queries ?? config.views ?? {})) {
|
|
192
|
-
console.log(` - ${name}`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch (err: any) {
|
|
196
|
-
console.error(`Error: ${err.message}`)
|
|
197
|
-
process.exit(1)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function printDdlActions(actions: { type: string; resource: string; name: string; detail?: string }[]): void {
|
|
202
|
-
const changed = actions.filter(a => a.type !== 'unchanged')
|
|
203
|
-
const unchanged = actions.filter(a => a.type === 'unchanged')
|
|
204
|
-
|
|
205
|
-
for (const action of actions) {
|
|
206
|
-
const icon = action.type === 'create' ? '+' : action.type === 'delete' ? '-' : ' '
|
|
207
|
-
const label = `${action.resource} '${action.name}'`
|
|
208
|
-
const detail = action.detail ? ` (${action.detail})` : ''
|
|
209
|
-
|
|
210
|
-
if (action.type === 'unchanged') {
|
|
211
|
-
console.log(` ${icon} ${label}`)
|
|
212
|
-
} else if (action.type === 'create') {
|
|
213
|
-
console.log(` ${icon} ${label} - created${detail}`)
|
|
214
|
-
} else if (action.type === 'delete') {
|
|
215
|
-
console.log(` ${icon} ${label} - removed${detail}`)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
console.log(`\n${changed.length} changed, ${unchanged.length} unchanged`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function getConfigPath(): string {
|
|
223
|
-
const idx = process.argv.indexOf('--config')
|
|
224
|
-
if (idx !== -1 && process.argv[idx + 1]) {
|
|
225
|
-
return process.argv[idx + 1]
|
|
226
|
-
}
|
|
227
|
-
return './wavelet.config.ts'
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
main().catch((err) => {
|
|
231
|
-
console.error('Error:', err.message)
|
|
232
|
-
process.exit(1)
|
|
233
|
-
})
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { execSync, spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
-
import pg from 'pg'
|
|
3
|
-
|
|
4
|
-
const { Client } = pg
|
|
5
|
-
|
|
6
|
-
export async function ensureRisingWave(connectionString: string): Promise<ChildProcess | null> {
|
|
7
|
-
// Try to connect to existing RisingWave
|
|
8
|
-
if (await isReachable(connectionString)) {
|
|
9
|
-
console.log('RisingWave is already running.')
|
|
10
|
-
return null
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
console.log('RisingWave is not reachable. Attempting to start...')
|
|
14
|
-
|
|
15
|
-
// Try native binary first, then docker
|
|
16
|
-
const binary = findBinary()
|
|
17
|
-
if (binary) {
|
|
18
|
-
return startNative(binary)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (hasDocker()) {
|
|
22
|
-
return startDocker()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
console.error(
|
|
26
|
-
'Could not start RisingWave.\n\n' +
|
|
27
|
-
'Install one of:\n' +
|
|
28
|
-
' brew tap risingwavelabs/risingwave && brew install risingwave\n' +
|
|
29
|
-
' docker pull risingwavelabs/risingwave:latest\n\n' +
|
|
30
|
-
'Or start RisingWave manually and re-run wavelet dev.'
|
|
31
|
-
)
|
|
32
|
-
process.exit(1)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function isReachable(connectionString: string): Promise<boolean> {
|
|
36
|
-
const client = new Client({ connectionString, connectionTimeoutMillis: 3000 })
|
|
37
|
-
try {
|
|
38
|
-
await client.connect()
|
|
39
|
-
await client.query('SELECT 1')
|
|
40
|
-
await client.end()
|
|
41
|
-
return true
|
|
42
|
-
} catch {
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function findBinary(): string | null {
|
|
48
|
-
try {
|
|
49
|
-
const path = execSync('which risingwave', { encoding: 'utf-8' }).trim()
|
|
50
|
-
return path || null
|
|
51
|
-
} catch {
|
|
52
|
-
return null
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function hasDocker(): boolean {
|
|
57
|
-
try {
|
|
58
|
-
execSync('docker info', { stdio: 'ignore' })
|
|
59
|
-
return true
|
|
60
|
-
} catch {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function startNative(binaryPath: string): Promise<ChildProcess> {
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
67
|
-
console.log(`Starting RisingWave (${binaryPath})...`)
|
|
68
|
-
const child = spawn(binaryPath, ['playground'], {
|
|
69
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
-
detached: false,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
let started = false
|
|
74
|
-
|
|
75
|
-
const onData = (data: Buffer) => {
|
|
76
|
-
const text = data.toString()
|
|
77
|
-
if (!started && text.includes('ready to accept connections')) {
|
|
78
|
-
started = true
|
|
79
|
-
console.log('RisingWave started (playground mode).')
|
|
80
|
-
resolve(child)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
child.stdout?.on('data', onData)
|
|
85
|
-
child.stderr?.on('data', onData)
|
|
86
|
-
|
|
87
|
-
child.on('error', (err) => {
|
|
88
|
-
if (!started) reject(err)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
child.on('exit', (code) => {
|
|
92
|
-
if (!started) reject(new Error(`RisingWave exited with code ${code}`))
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
// Fallback: poll for connectivity
|
|
96
|
-
const poll = setInterval(async () => {
|
|
97
|
-
if (started) {
|
|
98
|
-
clearInterval(poll)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
if (await isReachable('postgres://root@localhost:4566/dev')) {
|
|
102
|
-
started = true
|
|
103
|
-
clearInterval(poll)
|
|
104
|
-
console.log('RisingWave started (playground mode).')
|
|
105
|
-
resolve(child)
|
|
106
|
-
}
|
|
107
|
-
}, 1000)
|
|
108
|
-
|
|
109
|
-
// Timeout after 30 seconds
|
|
110
|
-
setTimeout(() => {
|
|
111
|
-
if (!started) {
|
|
112
|
-
clearInterval(poll)
|
|
113
|
-
child.kill()
|
|
114
|
-
reject(new Error('RisingWave failed to start within 30 seconds.'))
|
|
115
|
-
}
|
|
116
|
-
}, 30000)
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function startDocker(): Promise<ChildProcess> {
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
console.log('Starting RisingWave via Docker...')
|
|
123
|
-
|
|
124
|
-
// Remove stale container if exists
|
|
125
|
-
try {
|
|
126
|
-
execSync('docker rm -f wavelet-risingwave', { stdio: 'ignore' })
|
|
127
|
-
} catch {}
|
|
128
|
-
|
|
129
|
-
const child = spawn('docker', [
|
|
130
|
-
'run', '--name', 'wavelet-risingwave',
|
|
131
|
-
'-p', '4566:4566',
|
|
132
|
-
'risingwavelabs/risingwave:latest',
|
|
133
|
-
'playground',
|
|
134
|
-
], {
|
|
135
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
136
|
-
detached: false,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
let started = false
|
|
140
|
-
|
|
141
|
-
// Poll for connectivity
|
|
142
|
-
const poll = setInterval(async () => {
|
|
143
|
-
if (started) {
|
|
144
|
-
clearInterval(poll)
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
if (await isReachable('postgres://root@localhost:4566/dev')) {
|
|
148
|
-
started = true
|
|
149
|
-
clearInterval(poll)
|
|
150
|
-
console.log('RisingWave started via Docker.')
|
|
151
|
-
resolve(child)
|
|
152
|
-
}
|
|
153
|
-
}, 1000)
|
|
154
|
-
|
|
155
|
-
child.on('error', (err) => {
|
|
156
|
-
if (!started) {
|
|
157
|
-
clearInterval(poll)
|
|
158
|
-
reject(err)
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
child.on('exit', (code) => {
|
|
163
|
-
if (!started) {
|
|
164
|
-
clearInterval(poll)
|
|
165
|
-
reject(new Error(`Docker exited with code ${code}`))
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
setTimeout(() => {
|
|
170
|
-
if (!started) {
|
|
171
|
-
clearInterval(poll)
|
|
172
|
-
child.kill()
|
|
173
|
-
reject(new Error('RisingWave Docker container failed to start within 60 seconds.'))
|
|
174
|
-
}
|
|
175
|
-
}, 60000)
|
|
176
|
-
})
|
|
177
|
-
}
|