@beechcms/cli 0.6.0-preview.2 → 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 +5 -0
- package/.turbo/turbo-lint.log +1 -0
- package/coverage/coverage-summary.json +3 -0
- package/dist/index.js +563 -230
- package/package.json +12 -11
- 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/reset.ts +157 -0
- 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 -17
- 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/reset.test.ts +128 -0
- package/src/test/schema-diff.test.ts +232 -0
- package/src/test/seed-load.test.ts +158 -0
- package/src/test/validate.test.ts +261 -0
- 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
|
@@ -0,0 +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
|
+
})
|
|
@@ -0,0 +1,261 @@
|
|
|
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 { validateSeeds, validate } from '../commands/validate.js'
|
|
6
|
+
import type { Seed } from '@beechcms/core'
|
|
7
|
+
|
|
8
|
+
type PartialBranch = Omit<Seed['branches'][number], 'id'>
|
|
9
|
+
|
|
10
|
+
function makeSeed(slug: string, branches: PartialBranch[] = []): Seed {
|
|
11
|
+
let counter = 0
|
|
12
|
+
const withIds = (branches.length > 0 ? branches : [{ alias: 'title', label: 'Title', type: 'text' }])
|
|
13
|
+
.map(b => ({ id: `br_${String(++counter).padStart(2, '0')}`, ...b }))
|
|
14
|
+
return {
|
|
15
|
+
slug,
|
|
16
|
+
label: slug,
|
|
17
|
+
displayNameAlias: withIds[0]?.alias ?? 'title',
|
|
18
|
+
branches: withIds,
|
|
19
|
+
} as Seed
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Unknown relation target ────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe('validateSeeds — unknown relation target', () => {
|
|
25
|
+
it('emits a fatal error when targetSeed not in registry', () => {
|
|
26
|
+
const registry = {
|
|
27
|
+
articles: makeSeed('articles', [
|
|
28
|
+
{ alias: 'title', label: 'Title', type: 'text' },
|
|
29
|
+
{ alias: 'author_id', label: 'Author', type: 'relation', targetSeed: 'team' },
|
|
30
|
+
]),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
34
|
+
const fatal = errors.filter(e => e.fatal)
|
|
35
|
+
|
|
36
|
+
expect(fatal).toHaveLength(1)
|
|
37
|
+
expect(fatal[0].slug).toBe('articles')
|
|
38
|
+
expect(fatal[0].messages[0]).toContain('author_id')
|
|
39
|
+
expect(fatal[0].messages[0]).toContain('team')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('no fatal error when targetSeed is in registry', () => {
|
|
43
|
+
const registry = {
|
|
44
|
+
team: makeSeed('team', [{ alias: 'name', label: 'Name', type: 'text' }]),
|
|
45
|
+
articles: makeSeed('articles', [
|
|
46
|
+
{ alias: 'title', label: 'Title', type: 'text' },
|
|
47
|
+
{ alias: 'author_id', label: 'Author', type: 'relation', targetSeed: 'team' },
|
|
48
|
+
]),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
52
|
+
const fatal = errors.filter(e => e.fatal)
|
|
53
|
+
expect(fatal).toHaveLength(0)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// ── Cyclic dependency ─────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe('validateSeeds — cyclic dependency', () => {
|
|
60
|
+
it('emits a fatal error listing both slugs in a cycle', () => {
|
|
61
|
+
const registry = {
|
|
62
|
+
a: {
|
|
63
|
+
slug: 'a',
|
|
64
|
+
label: 'A',
|
|
65
|
+
displayNameAlias: 'title',
|
|
66
|
+
branches: [
|
|
67
|
+
{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' },
|
|
68
|
+
{ id: 'br_02', alias: 'b_id', label: 'B', type: 'relation', targetSeed: 'b' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
b: {
|
|
72
|
+
slug: 'b',
|
|
73
|
+
label: 'B',
|
|
74
|
+
displayNameAlias: 'title',
|
|
75
|
+
branches: [
|
|
76
|
+
{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' },
|
|
77
|
+
{ id: 'br_02', alias: 'a_id', label: 'A', type: 'relation', targetSeed: 'a' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
83
|
+
const fatal = errors.filter(e => e.fatal)
|
|
84
|
+
|
|
85
|
+
// One fatal entry for the cycle
|
|
86
|
+
expect(fatal.length).toBeGreaterThan(0)
|
|
87
|
+
const cycleMsg = fatal.find(e => e.messages.some(m => m.includes('Cyclic')))
|
|
88
|
+
expect(cycleMsg).toBeDefined()
|
|
89
|
+
expect(cycleMsg!.messages[0]).toContain('a')
|
|
90
|
+
expect(cycleMsg!.messages[0]).toContain('b')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ── Multi-relation: SET NULL rejection ───────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe('validateSeeds — multi-relation SET NULL', () => {
|
|
97
|
+
it('emits a fatal error when multiple: true + onDelete: SET NULL', () => {
|
|
98
|
+
const registry = {
|
|
99
|
+
tag: makeSeed('tag'),
|
|
100
|
+
articles: makeSeed('articles', [
|
|
101
|
+
{ alias: 'title', label: 'Title', type: 'text' },
|
|
102
|
+
{
|
|
103
|
+
alias: 'tags',
|
|
104
|
+
label: 'Tags',
|
|
105
|
+
type: 'relation',
|
|
106
|
+
targetSeed: 'tag',
|
|
107
|
+
multiple: true,
|
|
108
|
+
onDelete: 'SET NULL',
|
|
109
|
+
},
|
|
110
|
+
]),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
114
|
+
const fatal = errors.filter(e => e.fatal)
|
|
115
|
+
expect(fatal.length).toBeGreaterThan(0)
|
|
116
|
+
const tagError = fatal.find(e => e.slug === 'articles')
|
|
117
|
+
expect(tagError).toBeDefined()
|
|
118
|
+
expect(tagError!.messages[0]).toContain("'tags'")
|
|
119
|
+
expect(tagError!.messages[0]).toContain('SET NULL')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('does not error for multiple: true + onDelete: CASCADE', () => {
|
|
123
|
+
const registry = {
|
|
124
|
+
tag: makeSeed('tag'),
|
|
125
|
+
articles: makeSeed('articles', [
|
|
126
|
+
{ alias: 'title', label: 'Title', type: 'text' },
|
|
127
|
+
{
|
|
128
|
+
alias: 'tags',
|
|
129
|
+
label: 'Tags',
|
|
130
|
+
type: 'relation',
|
|
131
|
+
targetSeed: 'tag',
|
|
132
|
+
multiple: true,
|
|
133
|
+
onDelete: 'CASCADE',
|
|
134
|
+
},
|
|
135
|
+
]),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
139
|
+
const setNullErrors = errors.filter(e => e.fatal && e.messages.some(m => m.includes('SET NULL')))
|
|
140
|
+
expect(setNullErrors).toHaveLength(0)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('does not error for multiple: true with no onDelete (defaults to CASCADE)', () => {
|
|
144
|
+
const registry = {
|
|
145
|
+
tag: makeSeed('tag'),
|
|
146
|
+
articles: makeSeed('articles', [
|
|
147
|
+
{ alias: 'title', label: 'Title', type: 'text' },
|
|
148
|
+
{ alias: 'tags', label: 'Tags', type: 'relation', targetSeed: 'tag', multiple: true },
|
|
149
|
+
]),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
153
|
+
const fatal = errors.filter(e => e.fatal)
|
|
154
|
+
expect(fatal).toHaveLength(0)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ── Multi-relation: junction table name collision ─────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe('validateSeeds — junction table name collision', () => {
|
|
161
|
+
it('emits fatal error when two branches produce the same junction table name', () => {
|
|
162
|
+
// 'rel_articles_tags' collides if somehow two branches have the same alias
|
|
163
|
+
// This is prevented by duplicate alias check, but the junction collision check
|
|
164
|
+
// handles cross-seed collisions after truncation.
|
|
165
|
+
const registry = {
|
|
166
|
+
tag: makeSeed('tag'),
|
|
167
|
+
// Simulate a collision: same junction name from different seeds
|
|
168
|
+
rel_articles: makeSeed('rel_articles', [
|
|
169
|
+
{ alias: 'title', label: 'T', type: 'text' },
|
|
170
|
+
{ alias: 'tags', label: 'Tags', type: 'relation', targetSeed: 'tag', multiple: true },
|
|
171
|
+
]),
|
|
172
|
+
articles: makeSeed('articles', [
|
|
173
|
+
{ alias: 'title', label: 'T', type: 'text' },
|
|
174
|
+
// slug='articles' + alias='rel_articles_tags' → 'rel_articles_rel_articles_tags'
|
|
175
|
+
// Use a manually crafted collision scenario:
|
|
176
|
+
{ alias: 'tags', label: 'Tags', type: 'relation', targetSeed: 'tag', multiple: true },
|
|
177
|
+
]),
|
|
178
|
+
}
|
|
179
|
+
// These won't collide but the rule should run without errors
|
|
180
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
181
|
+
const collisionErrors = errors.filter(e => e.fatal && e.messages.some(m => m.includes('collision')))
|
|
182
|
+
expect(collisionErrors).toHaveLength(0)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ── Existing warning checks still work ────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe('validateSeeds — existing warning checks', () => {
|
|
189
|
+
it('flags missing displayNameAlias as warning (not fatal)', () => {
|
|
190
|
+
const registry = {
|
|
191
|
+
items: {
|
|
192
|
+
slug: 'items',
|
|
193
|
+
label: 'Items',
|
|
194
|
+
displayNameAlias: 'nonexistent',
|
|
195
|
+
branches: [{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' }],
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const errors = validateSeeds(registry as Record<string, Seed>)
|
|
200
|
+
const warnings = errors.filter(e => !e.fatal)
|
|
201
|
+
expect(warnings.length).toBeGreaterThan(0)
|
|
202
|
+
expect(warnings[0].messages[0]).toContain('displayNameAlias')
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('validate CLI command wrapper', () => {
|
|
207
|
+
let logSpy: any
|
|
208
|
+
let warnSpy: any
|
|
209
|
+
let exitSpy: any
|
|
210
|
+
|
|
211
|
+
beforeEach(() => {
|
|
212
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
213
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
214
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
afterEach(() => {
|
|
218
|
+
vi.restoreAllMocks()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('warns when registry is empty', async () => {
|
|
222
|
+
await validate({ registry: {} })
|
|
223
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
224
|
+
expect(warnSpy.mock.calls[0][0]).toContain('SEED_REGISTRY is empty')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('logs success when all seeds are valid', async () => {
|
|
228
|
+
const registry = {
|
|
229
|
+
articles: makeSeed('articles'),
|
|
230
|
+
}
|
|
231
|
+
await validate({ registry })
|
|
232
|
+
expect(logSpy).toHaveBeenCalled()
|
|
233
|
+
expect(logSpy.mock.calls.some((c: any[]) => c[0].includes('All seeds valid'))).toBe(true)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('logs warnings when seeds have non-fatal issues', async () => {
|
|
237
|
+
const registry = {
|
|
238
|
+
items: {
|
|
239
|
+
slug: 'items',
|
|
240
|
+
label: 'Items',
|
|
241
|
+
displayNameAlias: 'nonexistent',
|
|
242
|
+
branches: [{ id: 'br_01', alias: 'title', label: 'Title', type: 'text' }],
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
await validate({ registry: registry as any })
|
|
246
|
+
expect(logSpy).toHaveBeenCalled()
|
|
247
|
+
expect(logSpy.mock.calls.some((c: any[]) => c[0].includes('Found 1 warning'))).toBe(true)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('exits with 1 when seeds have fatal errors', async () => {
|
|
251
|
+
const registry = {
|
|
252
|
+
articles: makeSeed('articles', [
|
|
253
|
+
{ alias: 'title', label: 'Title', type: 'text' },
|
|
254
|
+
{ alias: 'author_id', label: 'Author', type: 'relation', targetSeed: 'team' },
|
|
255
|
+
]),
|
|
256
|
+
}
|
|
257
|
+
await validate({ registry: registry as any })
|
|
258
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
package/tsconfig.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"composite": true,
|
|
5
|
-
"declaration": true,
|
|
6
|
-
"declarationMap": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"module": "ESNext",
|
|
10
|
-
"moduleResolution": "Bundler",
|
|
11
|
-
"target": "ES2022"
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"],
|
|
14
|
-
"exclude": ["node_modules", "dist"],
|
|
15
|
-
"references": [{ "path": "../core" }]
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"target": "ES2022"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"],
|
|
15
|
+
"references": [{ "path": "../core" }]
|
|
16
|
+
}
|