@beechcms/cli 0.6.0-preview.3 → 0.6.0-preview.4
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/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-lint.log +1 -0
- package/coverage/coverage-summary.json +3 -0
- package/dist/index.js +445 -249
- package/package.json +3 -2
- package/src/commands/deploy.ts +126 -126
- package/src/commands/generate-types.ts +65 -0
- package/src/commands/init.ts +599 -599
- package/src/commands/onboard.ts +32 -32
- package/src/commands/schema-diff.ts +78 -0
- package/src/commands/seed-create.ts +192 -192
- package/src/commands/seed-load.ts +206 -235
- package/src/commands/update.ts +54 -54
- package/src/commands/validate.ts +80 -80
- package/src/index.ts +24 -20
- package/src/lib/migration-writer.ts +106 -0
- package/src/lib/schema-diff.ts +175 -150
- package/src/lib/wrangler.ts +129 -129
- package/src/test/generate-types.test.ts +58 -0
- package/src/test/schema-diff.test.ts +232 -0
- package/src/test/seed-load.test.ts +158 -158
- package/src/test/validate.test.ts +261 -261
- package/tsconfig.json +16 -16
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +33 -33
- 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
|
@@ -1,158 +1,158 @@
|
|
|
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
|
-
})
|
|
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
|
+
})
|