@beechcms/cli 0.6.0-preview.3 → 0.6.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/index.js +1298 -765
- package/package.json +8 -3
- package/.turbo/turbo-build.log +0 -5
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -116
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -116
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/validate.ts.html +0 -325
- package/coverage/lcov.info +0 -80
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/validate.ts.html +0 -325
- package/dist/commands/seed-load.d.ts +0 -8
- package/dist/commands/seed-load.d.ts.map +0 -1
- package/dist/commands/seed-load.js +0 -89
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/schema-diff.d.ts +0 -15
- package/dist/lib/schema-diff.d.ts.map +0 -1
- package/dist/lib/schema-diff.js +0 -37
- package/dist/lib/wrangler.d.ts +0 -17
- package/dist/lib/wrangler.d.ts.map +0 -1
- package/dist/lib/wrangler.js +0 -65
- package/src/commands/deploy.ts +0 -126
- package/src/commands/init.ts +0 -599
- package/src/commands/onboard.ts +0 -32
- package/src/commands/reset.ts +0 -157
- package/src/commands/seed-create.ts +0 -192
- package/src/commands/seed-load.ts +0 -235
- package/src/commands/update.ts +0 -54
- package/src/commands/validate.ts +0 -80
- package/src/index.ts +0 -20
- package/src/lib/schema-diff.ts +0 -150
- package/src/lib/wrangler.ts +0 -129
- package/src/test/reset.test.ts +0 -128
- package/src/test/seed-load.test.ts +0 -158
- package/src/test/validate.test.ts +0 -261
- package/tsconfig.json +0 -16
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -33
package/src/commands/validate.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import pc from 'picocolors'
|
|
5
|
-
import type { Seed } from '@beechcms/core'
|
|
6
|
-
import { SEED_REGISTRY, validateSeedDefinitions } from '@beechcms/core'
|
|
7
|
-
|
|
8
|
-
export interface ValidateOptions {
|
|
9
|
-
registry?: Record<string, Seed> | null
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface SeedValidationError {
|
|
13
|
-
slug: string
|
|
14
|
-
messages: string[]
|
|
15
|
-
/** true = abort seed:load; false = warning only */
|
|
16
|
-
fatal: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function validateSeeds(registry: Record<string, Seed>): SeedValidationError[] {
|
|
20
|
-
return validateSeedDefinitions(Object.values(registry))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function validate(args: ValidateOptions): Promise<void> {
|
|
24
|
-
const registry = args.registry ?? SEED_REGISTRY
|
|
25
|
-
|
|
26
|
-
if (Object.keys(registry).length === 0) {
|
|
27
|
-
console.warn(pc.yellow('\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n'))
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
console.log(pc.cyan('\n beech validate — checking seeds\n'))
|
|
32
|
-
|
|
33
|
-
const errors = validateSeeds(registry)
|
|
34
|
-
const fatalErrors = errors.filter(e => e.fatal)
|
|
35
|
-
const warnings = errors.filter(e => !e.fatal)
|
|
36
|
-
|
|
37
|
-
// Print fatal errors first
|
|
38
|
-
for (const e of fatalErrors) {
|
|
39
|
-
console.log(pc.red(` ✗ ${e.slug} (fatal)`))
|
|
40
|
-
for (const msg of e.messages) {
|
|
41
|
-
console.log(pc.red(` → ${msg}`))
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Print per-seed warnings
|
|
46
|
-
const warningMap = new Map(warnings.map(e => [e.slug, e.messages]))
|
|
47
|
-
const allWarningSlugsSeen = new Set(warnings.map(e => e.slug))
|
|
48
|
-
|
|
49
|
-
for (const seed of Object.values(registry)) {
|
|
50
|
-
const msgs = warningMap.get(seed.slug)
|
|
51
|
-
if (!msgs) {
|
|
52
|
-
if (!allWarningSlugsSeen.has(seed.slug)) {
|
|
53
|
-
// only print ✓ if no fatal error for this slug either
|
|
54
|
-
const hasFatal = fatalErrors.some(e => e.slug === seed.slug)
|
|
55
|
-
if (!hasFatal) console.log(pc.green(` ✓ ${seed.slug}`))
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
console.log(pc.yellow(` ⚠ ${seed.slug}`))
|
|
59
|
-
for (const msg of msgs) {
|
|
60
|
-
console.log(pc.yellow(` → ${msg}`))
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
console.log('')
|
|
66
|
-
|
|
67
|
-
const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
|
|
68
|
-
const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0)
|
|
69
|
-
|
|
70
|
-
if (totalFatal > 0) {
|
|
71
|
-
const s = totalFatal !== 1 ? 's' : ''
|
|
72
|
-
console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.\n`))
|
|
73
|
-
process.exit(1)
|
|
74
|
-
} else if (totalWarnings > 0) {
|
|
75
|
-
const s = totalWarnings !== 1 ? 's' : ''
|
|
76
|
-
console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.\n`))
|
|
77
|
-
} else {
|
|
78
|
-
console.log(pc.green(' All seeds valid.\n'))
|
|
79
|
-
}
|
|
80
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
export { seedLoad } from './commands/seed-load.js'
|
|
5
|
-
export type { SeedLoadOptions } from './commands/seed-load.js'
|
|
6
|
-
export { init } from './commands/init.js'
|
|
7
|
-
export type { InitOptions } from './commands/init.js'
|
|
8
|
-
export { validate, validateSeeds } from './commands/validate.js'
|
|
9
|
-
export type { ValidateOptions, SeedValidationError } from './commands/validate.js'
|
|
10
|
-
export { seedCreate } from './commands/seed-create.js'
|
|
11
|
-
export type { SeedCreateOptions } from './commands/seed-create.js'
|
|
12
|
-
export { deploy } from './commands/deploy.js'
|
|
13
|
-
export type { DeployOptions } from './commands/deploy.js'
|
|
14
|
-
export { onboard } from './commands/onboard.js'
|
|
15
|
-
export type { OnboardOptions } from './commands/onboard.js'
|
|
16
|
-
export { update } from './commands/update.js'
|
|
17
|
-
export type { UpdateOptions } from './commands/update.js'
|
|
18
|
-
export { reset } from './commands/reset.js'
|
|
19
|
-
export type { ResetOptions } from './commands/reset.js'
|
|
20
|
-
|
package/src/lib/schema-diff.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import type { Seed } from '@beechcms/core'
|
|
5
|
-
import { getExpectedColumns, type SchemaColumn } from '@beechcms/core'
|
|
6
|
-
import type { WranglerOptions, D1Row } from './wrangler.js'
|
|
7
|
-
import { queryD1 } from './wrangler.js'
|
|
8
|
-
|
|
9
|
-
interface PragmaRow extends D1Row {
|
|
10
|
-
name: string
|
|
11
|
-
type: string
|
|
12
|
-
notnull: number
|
|
13
|
-
pk: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface FkRow extends D1Row {
|
|
17
|
-
id: number
|
|
18
|
-
seq: number
|
|
19
|
-
table: string
|
|
20
|
-
from: string
|
|
21
|
-
to: string
|
|
22
|
-
on_update: string
|
|
23
|
-
on_delete: string
|
|
24
|
-
match: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface IndexRow extends D1Row {
|
|
28
|
-
seq: number
|
|
29
|
-
name: string
|
|
30
|
-
unique: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ColumnDiff {
|
|
34
|
-
name: string
|
|
35
|
-
status: 'ok' | 'missing' | 'extra' | 'type_mismatch' | 'fk_missing' | 'fk_mismatch' | 'index_missing'
|
|
36
|
-
expectedType?: string
|
|
37
|
-
actualType?: string
|
|
38
|
-
/** For fk_missing/fk_mismatch: expected FK target table */
|
|
39
|
-
expectedTarget?: string
|
|
40
|
-
/** For fk_mismatch: what the DB actually has */
|
|
41
|
-
expected?: string
|
|
42
|
-
actual?: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SeedDiff {
|
|
46
|
-
slug: string
|
|
47
|
-
tableExists: boolean
|
|
48
|
-
columns: ColumnDiff[]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff> {
|
|
52
|
-
const tableName = `content_${seed.slug}`
|
|
53
|
-
const expected = getExpectedColumns(seed)
|
|
54
|
-
|
|
55
|
-
let actual: PragmaRow[]
|
|
56
|
-
try {
|
|
57
|
-
actual = queryD1<PragmaRow>(`PRAGMA table_info(${tableName})`, options)
|
|
58
|
-
} catch {
|
|
59
|
-
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (actual.length === 0) {
|
|
63
|
-
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const actualMap = new Map<string, PragmaRow>(actual.map(r => [r.name, r]))
|
|
67
|
-
const expectedSet = new Set<string>(expected.map(c => c.name))
|
|
68
|
-
|
|
69
|
-
const columns: ColumnDiff[] = []
|
|
70
|
-
|
|
71
|
-
// ── Column presence + type checks ────────────────────────────────────────
|
|
72
|
-
for (const col of expected) {
|
|
73
|
-
const actualRow = actualMap.get(col.name)
|
|
74
|
-
if (!actualRow) {
|
|
75
|
-
columns.push({ name: col.name, status: 'missing', expectedType: col.sqlType })
|
|
76
|
-
} else if (actualRow.type.toUpperCase() !== col.sqlType) {
|
|
77
|
-
columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actualRow.type })
|
|
78
|
-
} else {
|
|
79
|
-
columns.push({ name: col.name, status: 'ok' })
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const row of actual) {
|
|
84
|
-
if (!expectedSet.has(row.name)) {
|
|
85
|
-
columns.push({ name: row.name, status: 'extra', actualType: row.type })
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── FK + index checks for relation branches ──────────────────────────────
|
|
90
|
-
const relationBranches = seed.branches.filter(b => b.type === 'relation' && b.targetSeed)
|
|
91
|
-
|
|
92
|
-
if (relationBranches.length > 0) {
|
|
93
|
-
let fkList: FkRow[] = []
|
|
94
|
-
let indexList: IndexRow[] = []
|
|
95
|
-
try {
|
|
96
|
-
fkList = queryD1<FkRow>(`PRAGMA foreign_key_list(${tableName})`, options)
|
|
97
|
-
indexList = queryD1<IndexRow>(`PRAGMA index_list(${tableName})`, options)
|
|
98
|
-
} catch {
|
|
99
|
-
// If PRAGMA fails (table may not exist yet), skip FK checks
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Build maps for fast lookup
|
|
103
|
-
// fkList has one row per FK column; `from` = local col, `table` = referenced table
|
|
104
|
-
const fkByCol = new Map<string, FkRow>()
|
|
105
|
-
for (const fk of fkList) {
|
|
106
|
-
fkByCol.set(fk.from, fk)
|
|
107
|
-
}
|
|
108
|
-
const indexNames = new Set(indexList.map(i => i.name))
|
|
109
|
-
|
|
110
|
-
for (const branch of relationBranches) {
|
|
111
|
-
const expectedFkTable = `content_${branch.targetSeed}`
|
|
112
|
-
const expectedOnDelete = (branch.onDelete ?? 'SET NULL').toUpperCase()
|
|
113
|
-
const expectedIndexName = `idx_${seed.slug}_${branch.alias}`
|
|
114
|
-
|
|
115
|
-
// Find column diff entry for this branch (already evaluated above)
|
|
116
|
-
const colDiff = columns.find(c => c.name === branch.alias)
|
|
117
|
-
if (!colDiff || colDiff.status === 'missing') continue // already flagged
|
|
118
|
-
|
|
119
|
-
const fk = fkByCol.get(branch.alias)
|
|
120
|
-
|
|
121
|
-
if (!fk) {
|
|
122
|
-
// Column exists but no FK
|
|
123
|
-
colDiff.status = 'fk_missing'
|
|
124
|
-
colDiff.expectedTarget = branch.targetSeed
|
|
125
|
-
} else {
|
|
126
|
-
const actualTable = fk.table
|
|
127
|
-
const actualOnDelete = fk.on_delete.toUpperCase()
|
|
128
|
-
if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
|
|
129
|
-
colDiff.status = 'fk_mismatch'
|
|
130
|
-
colDiff.expected = `→ ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`
|
|
131
|
-
colDiff.actual = `→ ${actualTable}(id) ON DELETE ${actualOnDelete}`
|
|
132
|
-
colDiff.expectedTarget = branch.targetSeed
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Check index separately (can coexist with fk status)
|
|
137
|
-
if (!indexNames.has(expectedIndexName)) {
|
|
138
|
-
// Only add index_missing if the column is otherwise OK (FK issue takes precedence)
|
|
139
|
-
if (colDiff.status === 'ok') {
|
|
140
|
-
colDiff.status = 'index_missing'
|
|
141
|
-
} else {
|
|
142
|
-
// Append index info to the existing diff row as a separate entry
|
|
143
|
-
columns.push({ name: branch.alias, status: 'index_missing' })
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return { slug: seed.slug, tableExists: true, columns }
|
|
150
|
-
}
|
package/src/lib/wrangler.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import { spawnSync, type SpawnSyncReturns } from 'node:child_process'
|
|
5
|
-
import { writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
6
|
-
import { tmpdir } from 'node:os'
|
|
7
|
-
import { join, resolve } from 'node:path'
|
|
8
|
-
|
|
9
|
-
export interface WranglerOptions {
|
|
10
|
-
db: string
|
|
11
|
-
local: boolean
|
|
12
|
-
configPath: string | null
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface D1Row {
|
|
16
|
-
[key: string]: unknown
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface WranglerResult {
|
|
20
|
-
results: D1Row[]
|
|
21
|
-
success: boolean
|
|
22
|
-
error?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function buildArgs(options: WranglerOptions): string[] {
|
|
26
|
-
const args: string[] = []
|
|
27
|
-
if (options.configPath) args.push('--config', options.configPath)
|
|
28
|
-
if (options.local) args.push('--local')
|
|
29
|
-
else args.push('--remote')
|
|
30
|
-
return args
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Esegue SQL da file temporaneo via `wrangler d1 execute --file`. Returns true on success. */
|
|
34
|
-
export function executeD1File(sql: string, options: WranglerOptions): boolean {
|
|
35
|
-
const tmpFile = join(tmpdir(), `beech-seed-${Date.now()}.sql`)
|
|
36
|
-
try {
|
|
37
|
-
writeFileSync(tmpFile, sql, 'utf-8')
|
|
38
|
-
const args = ['d1', 'execute', options.db, '--file', tmpFile, ...buildArgs(options)]
|
|
39
|
-
const result = spawnSync('npx', ['wrangler', ...args], { stdio: 'inherit', cwd: process.cwd(), shell: true })
|
|
40
|
-
return result.status === 0
|
|
41
|
-
} finally {
|
|
42
|
-
try { rmSync(tmpFile) } catch {}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Esegue una query SQL e ritorna i risultati come array di oggetti (--json).
|
|
48
|
-
*
|
|
49
|
-
* Passa il SQL via file temporaneo (`--file`) anziché `--command`: su Windows,
|
|
50
|
-
* `spawnSync` con `shell: true` non preserva le virgolette/spazi/virgole interni
|
|
51
|
-
* a un argomento inline, e wrangler riceve il comando spezzato in token sciolti
|
|
52
|
-
* ("Unknown arguments: name, FROM, sqlite_master, …"). Il file evita del tutto
|
|
53
|
-
* il riquoting della shell — stesso approccio di `executeD1File`.
|
|
54
|
-
*/
|
|
55
|
-
export function queryD1<T extends D1Row = D1Row>(sql: string, options: WranglerOptions): T[] {
|
|
56
|
-
const tmpFile = join(tmpdir(), `beech-query-${Date.now()}.sql`)
|
|
57
|
-
let result: SpawnSyncReturns<string>
|
|
58
|
-
try {
|
|
59
|
-
writeFileSync(tmpFile, sql, 'utf-8')
|
|
60
|
-
const args = ['d1', 'execute', options.db, '--file', tmpFile, '--json', ...buildArgs(options)]
|
|
61
|
-
result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd(), shell: true })
|
|
62
|
-
} finally {
|
|
63
|
-
try { rmSync(tmpFile) } catch {}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (result.status !== 0) {
|
|
67
|
-
throw new Error(`wrangler d1 execute failed:\n${result.stderr}`)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const parsed: WranglerResult[] = JSON.parse(result.stdout)
|
|
72
|
-
return (parsed[0]?.results ?? []) as T[]
|
|
73
|
-
} catch {
|
|
74
|
-
throw new Error(`Failed to parse wrangler JSON output:\n${result.stdout}`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Trova il path di wrangler.jsonc risalendo l'albero da CWD fino alla root del filesystem. */
|
|
79
|
-
export function findWranglerConfig(): string | null {
|
|
80
|
-
let dir = process.cwd()
|
|
81
|
-
while (true) {
|
|
82
|
-
for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
83
|
-
const p = resolve(dir, name)
|
|
84
|
-
if (existsSync(p)) return p
|
|
85
|
-
}
|
|
86
|
-
for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
87
|
-
const p = resolve(dir, 'apps', 'api', name)
|
|
88
|
-
if (existsSync(p)) return p
|
|
89
|
-
}
|
|
90
|
-
const parent = resolve(dir, '..')
|
|
91
|
-
if (parent === dir) break
|
|
92
|
-
dir = parent
|
|
93
|
-
}
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Wraps a string value in single quotes, escaping internal single quotes for SQL literals. */
|
|
98
|
-
export function sqlQuote(value: string): string {
|
|
99
|
-
return `'${value.replace(/'/g, "''")}'`
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Risolve il nome del database D1 da wrangler.jsonc (stripping JSONC comments). */
|
|
103
|
-
export function resolveDbName(configPath: string | null): string {
|
|
104
|
-
if (!configPath) return 'beech-db'
|
|
105
|
-
try {
|
|
106
|
-
const raw = readFileSync(configPath, 'utf-8')
|
|
107
|
-
|
|
108
|
-
if (configPath.endsWith('.toml')) {
|
|
109
|
-
// Basic regex-based TOML parsing for d1_databases
|
|
110
|
-
// Matches both [d1_databases] and [[d1_databases]]
|
|
111
|
-
const d1SectionMatch = raw.match(/\[\[?d1_databases\]\]?[\s\S]*?(?=\n\[|$)/)
|
|
112
|
-
if (d1SectionMatch) {
|
|
113
|
-
const section = d1SectionMatch[0]
|
|
114
|
-
const dbNameMatch = section.match(/database_name\s*=\s*["'](.+?)["']/)
|
|
115
|
-
if (dbNameMatch) return dbNameMatch[1]
|
|
116
|
-
}
|
|
117
|
-
return 'beech-db'
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const stripped = raw
|
|
121
|
-
.replace(/\/\/[^\n]*/g, '')
|
|
122
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
123
|
-
const parsed = JSON.parse(stripped)
|
|
124
|
-
const bindings: { database_name?: string }[] = parsed?.d1_databases ?? []
|
|
125
|
-
return bindings[0]?.database_name ?? 'beech-db'
|
|
126
|
-
} catch {
|
|
127
|
-
return 'beech-db'
|
|
128
|
-
}
|
|
129
|
-
}
|
package/src/test/reset.test.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
5
|
-
import { reset } from '../commands/reset.js'
|
|
6
|
-
import { spawnSync } from 'node:child_process'
|
|
7
|
-
import { existsSync } from 'node:fs'
|
|
8
|
-
|
|
9
|
-
vi.mock('node:child_process', () => ({
|
|
10
|
-
spawnSync: vi.fn(() => ({ status: 0 })),
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
|
-
vi.mock('node:fs', () => ({
|
|
14
|
-
existsSync: vi.fn(),
|
|
15
|
-
rmSync: vi.fn(),
|
|
16
|
-
readFileSync: vi.fn(() => '{}'),
|
|
17
|
-
}))
|
|
18
|
-
|
|
19
|
-
vi.mock('picocolors', () => ({
|
|
20
|
-
default: {
|
|
21
|
-
cyan: (s: string) => s,
|
|
22
|
-
dim: (s: string) => s,
|
|
23
|
-
green: (s: string) => s,
|
|
24
|
-
red: (s: string) => s,
|
|
25
|
-
yellow: (s: string) => s,
|
|
26
|
-
},
|
|
27
|
-
}))
|
|
28
|
-
|
|
29
|
-
describe('reset command', () => {
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
vi.clearAllMocks()
|
|
32
|
-
vi.mocked(spawnSync).mockImplementation(() => ({ status: 0 } as any))
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('runs docker compose down when --docker is passed and docker is running', async () => {
|
|
36
|
-
await reset({ docker: true })
|
|
37
|
-
expect(spawnSync).toHaveBeenCalledWith(
|
|
38
|
-
'docker',
|
|
39
|
-
['compose', 'down', '-v'],
|
|
40
|
-
expect.any(Object)
|
|
41
|
-
)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('does not run docker compose down when docker is not installed', async () => {
|
|
45
|
-
// Mock docker --version to fail
|
|
46
|
-
vi.mocked(spawnSync).mockImplementation((cmd: string, args?: any) => {
|
|
47
|
-
if (cmd === 'docker' && args && args[0] === '--version') {
|
|
48
|
-
return { status: 1 } as any
|
|
49
|
-
}
|
|
50
|
-
return { status: 0 } as any
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
|
54
|
-
try {
|
|
55
|
-
await reset({ docker: true })
|
|
56
|
-
expect(spawnSync).not.toHaveBeenCalledWith(
|
|
57
|
-
'docker',
|
|
58
|
-
['compose', 'down', '-v'],
|
|
59
|
-
expect.any(Object)
|
|
60
|
-
)
|
|
61
|
-
expect(mockExit).toHaveBeenCalledWith(1)
|
|
62
|
-
} finally {
|
|
63
|
-
mockExit.mockRestore()
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('does not run docker compose down when docker daemon is not running', async () => {
|
|
68
|
-
// Mock docker info to fail
|
|
69
|
-
vi.mocked(spawnSync).mockImplementation((cmd: string, args?: any) => {
|
|
70
|
-
if (cmd === 'docker' && args && args[0] === 'info') {
|
|
71
|
-
return { status: 1 } as any
|
|
72
|
-
}
|
|
73
|
-
return { status: 0 } as any
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
|
77
|
-
try {
|
|
78
|
-
await reset({ docker: true })
|
|
79
|
-
expect(spawnSync).not.toHaveBeenCalledWith(
|
|
80
|
-
'docker',
|
|
81
|
-
['compose', 'down', '-v'],
|
|
82
|
-
expect.any(Object)
|
|
83
|
-
)
|
|
84
|
-
expect(mockExit).toHaveBeenCalledWith(1)
|
|
85
|
-
} finally {
|
|
86
|
-
mockExit.mockRestore()
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('runs db reset using npm script when apps/api has package.json', async () => {
|
|
91
|
-
vi.mocked(existsSync).mockImplementation((path: any) => {
|
|
92
|
-
if (typeof path === 'string' && path.includes('apps') && path.includes('package.json')) {
|
|
93
|
-
return true
|
|
94
|
-
}
|
|
95
|
-
return false
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
await reset({ db: true })
|
|
99
|
-
expect(spawnSync).toHaveBeenCalledWith(
|
|
100
|
-
'npm',
|
|
101
|
-
['run', 'db:reset:local'],
|
|
102
|
-
expect.objectContaining({
|
|
103
|
-
cwd: expect.stringMatching(/apps[/\\]api/),
|
|
104
|
-
})
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('runs both docker and db when --all is passed', async () => {
|
|
109
|
-
vi.mocked(existsSync).mockImplementation((path: any) => {
|
|
110
|
-
if (typeof path === 'string' && path.includes('apps') && path.includes('package.json')) {
|
|
111
|
-
return true
|
|
112
|
-
}
|
|
113
|
-
return false
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
await reset({ all: true })
|
|
117
|
-
expect(spawnSync).toHaveBeenCalledWith(
|
|
118
|
-
'docker',
|
|
119
|
-
['compose', 'down', '-v'],
|
|
120
|
-
expect.any(Object)
|
|
121
|
-
)
|
|
122
|
-
expect(spawnSync).toHaveBeenCalledWith(
|
|
123
|
-
'npm',
|
|
124
|
-
['run', 'db:reset:local'],
|
|
125
|
-
expect.any(Object)
|
|
126
|
-
)
|
|
127
|
-
})
|
|
128
|
-
})
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
5
|
-
import { sortSeedsByDependencies } from '@beechcms/core'
|
|
6
|
-
import type { Seed } from '@beechcms/core'
|
|
7
|
-
import { sqlQuote } from '../lib/wrangler.js'
|
|
8
|
-
import { buildSeedRegistrationSql } from '../commands/seed-load.js'
|
|
9
|
-
|
|
10
|
-
// These seeds declare articles BEFORE team (arbitrary user order in seed.ts)
|
|
11
|
-
const TEAM_SEED: Seed = {
|
|
12
|
-
slug: 'team',
|
|
13
|
-
label: 'Team',
|
|
14
|
-
displayNameAlias: 'name',
|
|
15
|
-
branches: [{ alias: 'name', label: 'Name', type: 'text' }],
|
|
16
|
-
} as Seed
|
|
17
|
-
|
|
18
|
-
const ARTICLES_SEED: Seed = {
|
|
19
|
-
slug: 'articles',
|
|
20
|
-
label: 'Articles',
|
|
21
|
-
displayNameAlias: 'title',
|
|
22
|
-
branches: [
|
|
23
|
-
{ alias: 'title', label: 'Title', type: 'text' },
|
|
24
|
-
{ alias: 'author_id', label: 'Author', type: 'relation', targetSeed: 'team' },
|
|
25
|
-
],
|
|
26
|
-
} as Seed
|
|
27
|
-
|
|
28
|
-
describe('sortSeedsByDependencies — topological ordering', () => {
|
|
29
|
-
it('puts team before articles even when articles is declared first', () => {
|
|
30
|
-
// articles declared first — arbitrary insertion order
|
|
31
|
-
const sorted = sortSeedsByDependencies([ARTICLES_SEED, TEAM_SEED])
|
|
32
|
-
const slugs = sorted.map(s => s.slug)
|
|
33
|
-
expect(slugs.indexOf('team')).toBeLessThan(slugs.indexOf('articles'))
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('preserves order for seeds with no relations', () => {
|
|
37
|
-
const a = { slug: 'a', label: 'A', displayNameAlias: 'x', branches: [{ alias: 'x', label: 'X', type: 'text' }] } as Seed
|
|
38
|
-
const b = { slug: 'b', label: 'B', displayNameAlias: 'x', branches: [{ alias: 'x', label: 'X', type: 'text' }] } as Seed
|
|
39
|
-
const sorted = sortSeedsByDependencies([a, b])
|
|
40
|
-
expect(sorted).toHaveLength(2)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('throws on unknown targetSeed', () => {
|
|
44
|
-
const bad: Seed = {
|
|
45
|
-
slug: 'bad',
|
|
46
|
-
label: 'Bad',
|
|
47
|
-
displayNameAlias: 'title',
|
|
48
|
-
branches: [
|
|
49
|
-
{ alias: 'title', label: 'Title', type: 'text' },
|
|
50
|
-
{ alias: 'ref_id', label: 'Ref', type: 'relation', targetSeed: 'ghost' },
|
|
51
|
-
],
|
|
52
|
-
} as Seed
|
|
53
|
-
expect(() => sortSeedsByDependencies([bad])).toThrow(/unknown target|ghost/)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('throws on cyclic graph', () => {
|
|
57
|
-
const a = {
|
|
58
|
-
slug: 'a', label: 'A', displayNameAlias: 'x',
|
|
59
|
-
branches: [
|
|
60
|
-
{ alias: 'x', label: 'X', type: 'text' },
|
|
61
|
-
{ alias: 'b_id', label: 'B', type: 'relation', targetSeed: 'b' },
|
|
62
|
-
],
|
|
63
|
-
} as Seed
|
|
64
|
-
const b = {
|
|
65
|
-
slug: 'b', label: 'B', displayNameAlias: 'x',
|
|
66
|
-
branches: [
|
|
67
|
-
{ alias: 'x', label: 'X', type: 'text' },
|
|
68
|
-
{ alias: 'a_id', label: 'A', type: 'relation', targetSeed: 'a' },
|
|
69
|
-
],
|
|
70
|
-
} as Seed
|
|
71
|
-
expect(() => sortSeedsByDependencies([a, b])).toThrow(/[Cc]ycl/)
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// ── sqlQuote ─────────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
describe('sqlQuote', () => {
|
|
78
|
-
it('wraps value in single quotes', () => {
|
|
79
|
-
expect(sqlQuote('hello')).toBe("'hello'")
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it("escapes internal single quotes by doubling them", () => {
|
|
83
|
-
expect(sqlQuote("it's")).toBe("'it''s'")
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('handles multiple single quotes', () => {
|
|
87
|
-
expect(sqlQuote("a'b'c")).toBe("'a''b''c'")
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('handles empty string', () => {
|
|
91
|
-
expect(sqlQuote('')).toBe("''")
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
// ── buildSeedRegistrationSql ─────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
describe('buildSeedRegistrationSql', () => {
|
|
98
|
-
const SIMPLE_SEED: Seed = {
|
|
99
|
-
slug: 'posts',
|
|
100
|
-
label: 'Posts',
|
|
101
|
-
displayNameAlias: 'title',
|
|
102
|
-
branches: [{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' }],
|
|
103
|
-
} as Seed
|
|
104
|
-
|
|
105
|
-
it('produces INSERT … ON CONFLICT for the correct slug', () => {
|
|
106
|
-
const sql = buildSeedRegistrationSql(SIMPLE_SEED)
|
|
107
|
-
expect(sql).toContain("INSERT INTO seeds")
|
|
108
|
-
expect(sql).toContain("ON CONFLICT(slug) DO UPDATE SET")
|
|
109
|
-
expect(sql).toContain("'posts'")
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it("sets source to 'code'", () => {
|
|
113
|
-
const sql = buildSeedRegistrationSql(SIMPLE_SEED)
|
|
114
|
-
expect(sql).toContain("'code'")
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('escapes single quotes in slug and JSON literal', () => {
|
|
118
|
-
const seedWithApostrophe: Seed = {
|
|
119
|
-
...SIMPLE_SEED,
|
|
120
|
-
slug: "it's",
|
|
121
|
-
label: "It's",
|
|
122
|
-
}
|
|
123
|
-
const sql = buildSeedRegistrationSql(seedWithApostrophe)
|
|
124
|
-
// Slug value must have its single quote doubled
|
|
125
|
-
expect(sql).toContain("'it''s'")
|
|
126
|
-
// JSON label must also have its single quote doubled
|
|
127
|
-
expect(sql).toContain("It''s")
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('does not contain unescaped single quotes inside the JSON literal', () => {
|
|
131
|
-
const sql = buildSeedRegistrationSql(SIMPLE_SEED)
|
|
132
|
-
const jsonStart = sql.indexOf("VALUES (")
|
|
133
|
-
const jsonPart = sql.slice(jsonStart)
|
|
134
|
-
// Extract the JSON literal between the second pair of outer quotes
|
|
135
|
-
// Verify it round-trips back to the original seed
|
|
136
|
-
const inner = JSON.stringify(SIMPLE_SEED).replace(/'/g, "''")
|
|
137
|
-
expect(sql).toContain(inner)
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// ── Dry-run output ordering ───────────────────────────────────────────────
|
|
142
|
-
// Verify that seed-load uses sortSeedsByDependencies (not Object.values order)
|
|
143
|
-
// by testing the pure function behavior that underpins it.
|
|
144
|
-
|
|
145
|
-
describe('seed-load dry-run ordering contract', () => {
|
|
146
|
-
it('content_team CREATE TABLE appears before content_articles when articles declared first', () => {
|
|
147
|
-
// The dry-run loops over sortSeedsByDependencies(Object.values(registry)).
|
|
148
|
-
// We verify the sort result here — the integration is in seed-load.ts.
|
|
149
|
-
const registry = {
|
|
150
|
-
articles: ARTICLES_SEED, // declared first
|
|
151
|
-
team: TEAM_SEED,
|
|
152
|
-
}
|
|
153
|
-
const sorted = sortSeedsByDependencies(Object.values(registry))
|
|
154
|
-
const slugs = sorted.map(s => s.slug)
|
|
155
|
-
// team must come first so its CREATE TABLE is emitted before articles'
|
|
156
|
-
expect(slugs.indexOf('team')).toBeLessThan(slugs.indexOf('articles'))
|
|
157
|
-
})
|
|
158
|
-
})
|