@actuate-media/cli 0.4.1 → 0.5.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +69 -12
- package/CHANGELOG.md +58 -0
- package/dist/__tests__/db-init.test.d.ts +2 -0
- package/dist/__tests__/db-init.test.d.ts.map +1 -0
- package/dist/__tests__/db-init.test.js +127 -0
- package/dist/__tests__/db-init.test.js.map +1 -0
- package/dist/__tests__/db-sync.test.d.ts +2 -0
- package/dist/__tests__/db-sync.test.d.ts.map +1 -0
- package/dist/__tests__/db-sync.test.js +136 -0
- package/dist/__tests__/db-sync.test.js.map +1 -0
- package/dist/__tests__/deployment-diagnostics.test.js.map +1 -1
- package/dist/__tests__/init.test.js.map +1 -1
- package/dist/__tests__/schema-fragment.test.js +1 -1
- package/dist/__tests__/schema-fragment.test.js.map +1 -1
- package/dist/__tests__/seed.test.js.map +1 -1
- package/dist/commands/db-init.d.ts +19 -2
- package/dist/commands/db-init.d.ts.map +1 -1
- package/dist/commands/db-init.js +128 -306
- package/dist/commands/db-init.js.map +1 -1
- package/dist/commands/db-status.d.ts +1 -1
- package/dist/commands/db-status.d.ts.map +1 -1
- package/dist/commands/db-status.js +33 -33
- package/dist/commands/db-status.js.map +1 -1
- package/dist/commands/db-sync.d.ts +31 -0
- package/dist/commands/db-sync.d.ts.map +1 -0
- package/dist/commands/db-sync.js +195 -0
- package/dist/commands/db-sync.js.map +1 -0
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -41
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/export.d.ts +1 -1
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +32 -32
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/generate.d.ts +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +8 -8
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/import.d.ts +1 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +55 -58
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +18 -24
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/seed.d.ts +1 -1
- package/dist/commands/seed.d.ts.map +1 -1
- package/dist/commands/seed.js +156 -157
- package/dist/commands/seed.js.map +1 -1
- package/dist/commands/update-check.d.ts +1 -1
- package/dist/commands/update-check.d.ts.map +1 -1
- package/dist/commands/update-check.js +34 -27
- package/dist/commands/update-check.js.map +1 -1
- package/dist/commands/upgrade.d.ts +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +46 -34
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/deployment/diagnostics.d.ts.map +1 -1
- package/dist/deployment/diagnostics.js +7 -2
- package/dist/deployment/diagnostics.js.map +1 -1
- package/dist/index.js +17 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +5 -5
- package/dist/utils/logger.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/db-init.test.ts +155 -0
- package/src/__tests__/db-sync.test.ts +167 -0
- package/src/__tests__/deployment-diagnostics.test.ts +68 -60
- package/src/__tests__/init.test.ts +17 -17
- package/src/__tests__/schema-fragment.test.ts +29 -25
- package/src/__tests__/seed.test.ts +25 -25
- package/src/commands/db-init.ts +146 -319
- package/src/commands/db-status.ts +70 -68
- package/src/commands/db-sync.ts +227 -0
- package/src/commands/doctor.ts +102 -88
- package/src/commands/export.ts +65 -75
- package/src/commands/generate.ts +14 -16
- package/src/commands/import.ts +125 -140
- package/src/commands/init.ts +14 -14
- package/src/commands/migrate.ts +29 -35
- package/src/commands/seed.ts +294 -300
- package/src/commands/update-check.ts +77 -72
- package/src/commands/upgrade.ts +100 -85
- package/src/deployment/diagnostics.ts +86 -72
- package/src/index.ts +32 -30
- package/src/utils/logger.ts +10 -10
package/src/commands/seed.ts
CHANGED
|
@@ -1,414 +1,408 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { readFile } from
|
|
3
|
-
import { existsSync } from
|
|
4
|
-
import { createRequire } from
|
|
5
|
-
import path from
|
|
6
|
-
import { createInterface } from
|
|
7
|
-
import { pathToFileURL } from
|
|
8
|
-
import ora from
|
|
9
|
-
import { logger } from
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { createInterface } from 'node:readline/promises'
|
|
7
|
+
import { pathToFileURL } from 'node:url'
|
|
8
|
+
import ora from 'ora'
|
|
9
|
+
import { logger } from '../utils/logger.js'
|
|
10
10
|
|
|
11
11
|
async function confirm(question: string): Promise<boolean> {
|
|
12
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
13
|
-
const answer = await rl.question(`${question} (y/N) `)
|
|
14
|
-
rl.close()
|
|
15
|
-
return answer.trim().toLowerCase() ===
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
13
|
+
const answer = await rl.question(`${question} (y/N) `)
|
|
14
|
+
rl.close()
|
|
15
|
+
return answer.trim().toLowerCase() === 'y'
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const DEMO_PAGES = [
|
|
19
19
|
{
|
|
20
|
-
title:
|
|
21
|
-
slug:
|
|
22
|
-
content:
|
|
20
|
+
title: 'Home',
|
|
21
|
+
slug: 'home',
|
|
22
|
+
content: '<h1>Welcome</h1><p>Your homepage content goes here.</p>',
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
title:
|
|
26
|
-
slug:
|
|
27
|
-
content:
|
|
28
|
-
"<h1>About Us</h1><p>Learn more about our team and mission.</p>",
|
|
25
|
+
title: 'About',
|
|
26
|
+
slug: 'about',
|
|
27
|
+
content: '<h1>About Us</h1><p>Learn more about our team and mission.</p>',
|
|
29
28
|
},
|
|
30
29
|
{
|
|
31
|
-
title:
|
|
32
|
-
slug:
|
|
33
|
-
content:
|
|
34
|
-
"<h1>Contact</h1><p>Get in touch with us via the form below.</p>",
|
|
30
|
+
title: 'Contact',
|
|
31
|
+
slug: 'contact',
|
|
32
|
+
content: '<h1>Contact</h1><p>Get in touch with us via the form below.</p>',
|
|
35
33
|
},
|
|
36
34
|
{
|
|
37
|
-
title:
|
|
38
|
-
slug:
|
|
39
|
-
content:
|
|
40
|
-
"<h1>Privacy Policy</h1><p>Your privacy matters to us. Read our full policy.</p>",
|
|
35
|
+
title: 'Privacy Policy',
|
|
36
|
+
slug: 'privacy-policy',
|
|
37
|
+
content: '<h1>Privacy Policy</h1><p>Your privacy matters to us. Read our full policy.</p>',
|
|
41
38
|
},
|
|
42
39
|
{
|
|
43
|
-
title:
|
|
44
|
-
slug:
|
|
45
|
-
content:
|
|
46
|
-
"<h1>Terms of Service</h1><p>Please read the following terms carefully.</p>",
|
|
40
|
+
title: 'Terms of Service',
|
|
41
|
+
slug: 'terms',
|
|
42
|
+
content: '<h1>Terms of Service</h1><p>Please read the following terms carefully.</p>',
|
|
47
43
|
},
|
|
48
|
-
]
|
|
44
|
+
]
|
|
49
45
|
|
|
50
46
|
const DEMO_POSTS = [
|
|
51
47
|
{
|
|
52
|
-
title:
|
|
53
|
-
slug:
|
|
54
|
-
excerpt:
|
|
48
|
+
title: 'Getting Started with Actuate CMS',
|
|
49
|
+
slug: 'getting-started',
|
|
50
|
+
excerpt: 'Learn how to set up and configure your new CMS.',
|
|
55
51
|
content:
|
|
56
|
-
|
|
57
|
-
status:
|
|
52
|
+
'<h1>Getting Started</h1><p>Welcome to Actuate CMS. This guide walks you through initial setup.</p>',
|
|
53
|
+
status: 'PUBLISHED',
|
|
58
54
|
},
|
|
59
55
|
{
|
|
60
|
-
title:
|
|
61
|
-
slug:
|
|
62
|
-
excerpt:
|
|
56
|
+
title: 'Content Modeling Best Practices',
|
|
57
|
+
slug: 'content-modeling',
|
|
58
|
+
excerpt: 'Design your collections and fields for maximum flexibility.',
|
|
63
59
|
content:
|
|
64
|
-
|
|
65
|
-
status:
|
|
60
|
+
'<h1>Content Modeling</h1><p>Effective content modeling is the foundation of a great CMS.</p>',
|
|
61
|
+
status: 'PUBLISHED',
|
|
66
62
|
},
|
|
67
63
|
{
|
|
68
|
-
title:
|
|
69
|
-
slug:
|
|
70
|
-
excerpt:
|
|
71
|
-
content:
|
|
72
|
-
|
|
73
|
-
status: "PUBLISHED",
|
|
64
|
+
title: 'Working with Media',
|
|
65
|
+
slug: 'working-with-media',
|
|
66
|
+
excerpt: 'Upload, organize, and optimize your media assets.',
|
|
67
|
+
content: '<h1>Working with Media</h1><p>Actuate CMS provides powerful media management.</p>',
|
|
68
|
+
status: 'PUBLISHED',
|
|
74
69
|
},
|
|
75
70
|
{
|
|
76
|
-
title:
|
|
77
|
-
slug:
|
|
78
|
-
excerpt:
|
|
79
|
-
content:
|
|
80
|
-
|
|
81
|
-
status: "PUBLISHED",
|
|
71
|
+
title: 'SEO Optimization Tips',
|
|
72
|
+
slug: 'seo-optimization',
|
|
73
|
+
excerpt: 'Boost your search rankings with built-in SEO tools.',
|
|
74
|
+
content: '<h1>SEO Optimization</h1><p>Follow these tips to improve your site visibility.</p>',
|
|
75
|
+
status: 'PUBLISHED',
|
|
82
76
|
},
|
|
83
77
|
{
|
|
84
|
-
title:
|
|
85
|
-
slug:
|
|
86
|
-
excerpt:
|
|
87
|
-
content:
|
|
88
|
-
|
|
89
|
-
status: "PUBLISHED",
|
|
78
|
+
title: 'Building Custom Plugins',
|
|
79
|
+
slug: 'building-plugins',
|
|
80
|
+
excerpt: 'Extend Actuate CMS with your own plugins.',
|
|
81
|
+
content: '<h1>Building Plugins</h1><p>The plugin system lets you add custom functionality.</p>',
|
|
82
|
+
status: 'PUBLISHED',
|
|
90
83
|
},
|
|
91
84
|
{
|
|
92
|
-
title:
|
|
93
|
-
slug:
|
|
94
|
-
excerpt:
|
|
95
|
-
content:
|
|
96
|
-
|
|
97
|
-
status: "PUBLISHED",
|
|
85
|
+
title: 'API Reference Overview',
|
|
86
|
+
slug: 'api-reference',
|
|
87
|
+
excerpt: 'A comprehensive guide to the Actuate CMS REST API.',
|
|
88
|
+
content: '<h1>API Reference</h1><p>Use the API to integrate your content anywhere.</p>',
|
|
89
|
+
status: 'PUBLISHED',
|
|
98
90
|
},
|
|
99
91
|
{
|
|
100
|
-
title:
|
|
101
|
-
slug:
|
|
102
|
-
excerpt:
|
|
103
|
-
content:
|
|
104
|
-
|
|
105
|
-
status: "PUBLISHED",
|
|
92
|
+
title: 'Deployment Guide',
|
|
93
|
+
slug: 'deployment-guide',
|
|
94
|
+
excerpt: 'Deploy Actuate CMS to Vercel, AWS, or self-hosted.',
|
|
95
|
+
content: '<h1>Deployment Guide</h1><p>Multiple deployment options for every use case.</p>',
|
|
96
|
+
status: 'PUBLISHED',
|
|
106
97
|
},
|
|
107
98
|
{
|
|
108
|
-
title:
|
|
109
|
-
slug:
|
|
110
|
-
excerpt:
|
|
99
|
+
title: 'Multi-language Content',
|
|
100
|
+
slug: 'multi-language',
|
|
101
|
+
excerpt: 'Set up localization and manage translated content.',
|
|
111
102
|
content:
|
|
112
|
-
|
|
113
|
-
status:
|
|
103
|
+
'<h1>Multi-language Content</h1><p>Reach a global audience with localized content.</p>',
|
|
104
|
+
status: 'PUBLISHED',
|
|
114
105
|
},
|
|
115
106
|
{
|
|
116
|
-
title:
|
|
117
|
-
slug:
|
|
118
|
-
excerpt:
|
|
119
|
-
content:
|
|
120
|
-
|
|
121
|
-
status: "DRAFT",
|
|
107
|
+
title: 'Webhooks and Integrations',
|
|
108
|
+
slug: 'webhooks-integrations',
|
|
109
|
+
excerpt: 'Connect Actuate CMS to external services with webhooks.',
|
|
110
|
+
content: '<h1>Webhooks</h1><p>Automate workflows by connecting to third-party services.</p>',
|
|
111
|
+
status: 'DRAFT',
|
|
122
112
|
},
|
|
123
113
|
{
|
|
124
|
-
title:
|
|
125
|
-
slug:
|
|
126
|
-
excerpt:
|
|
127
|
-
content:
|
|
128
|
-
|
|
129
|
-
status: "DRAFT",
|
|
114
|
+
title: 'Advanced Access Control',
|
|
115
|
+
slug: 'access-control',
|
|
116
|
+
excerpt: 'Fine-tune permissions with role-based access control.',
|
|
117
|
+
content: '<h1>Access Control</h1><p>Protect your content with granular permissions.</p>',
|
|
118
|
+
status: 'DRAFT',
|
|
130
119
|
},
|
|
131
|
-
]
|
|
120
|
+
]
|
|
132
121
|
|
|
133
122
|
const DEMO_FORMS = [
|
|
134
123
|
{
|
|
135
|
-
title:
|
|
136
|
-
slug:
|
|
124
|
+
title: 'Contact Form',
|
|
125
|
+
slug: 'contact-form',
|
|
137
126
|
fields: [
|
|
138
|
-
{ name:
|
|
139
|
-
{ name:
|
|
140
|
-
{ name:
|
|
127
|
+
{ name: 'name', type: 'text', required: true },
|
|
128
|
+
{ name: 'email', type: 'email', required: true },
|
|
129
|
+
{ name: 'message', type: 'textarea', required: true },
|
|
141
130
|
],
|
|
142
|
-
submitLabel:
|
|
131
|
+
submitLabel: 'Send Message',
|
|
143
132
|
successMessage: "Thanks for reaching out! We'll get back to you soon.",
|
|
144
133
|
},
|
|
145
134
|
{
|
|
146
|
-
title:
|
|
147
|
-
slug:
|
|
135
|
+
title: 'Newsletter Signup',
|
|
136
|
+
slug: 'newsletter',
|
|
148
137
|
fields: [
|
|
149
|
-
{ name:
|
|
150
|
-
{ name:
|
|
138
|
+
{ name: 'email', type: 'email', required: true },
|
|
139
|
+
{ name: 'firstName', type: 'text', required: false },
|
|
151
140
|
],
|
|
152
|
-
submitLabel:
|
|
141
|
+
submitLabel: 'Subscribe',
|
|
153
142
|
successMessage: "You're subscribed! Check your inbox for confirmation.",
|
|
154
143
|
},
|
|
155
144
|
{
|
|
156
|
-
title:
|
|
157
|
-
slug:
|
|
145
|
+
title: 'Feedback Form',
|
|
146
|
+
slug: 'feedback',
|
|
158
147
|
fields: [
|
|
159
|
-
{ name:
|
|
160
|
-
{ name:
|
|
161
|
-
{ name:
|
|
148
|
+
{ name: 'name', type: 'text', required: false },
|
|
149
|
+
{ name: 'rating', type: 'select', options: ['1', '2', '3', '4', '5'], required: true },
|
|
150
|
+
{ name: 'comments', type: 'textarea', required: false },
|
|
162
151
|
],
|
|
163
|
-
submitLabel:
|
|
164
|
-
successMessage:
|
|
152
|
+
submitLabel: 'Submit Feedback',
|
|
153
|
+
successMessage: 'Thank you for your feedback!',
|
|
165
154
|
},
|
|
166
|
-
]
|
|
155
|
+
]
|
|
167
156
|
|
|
168
157
|
const DEMO_USERS = [
|
|
169
|
-
{ email:
|
|
170
|
-
{ email:
|
|
171
|
-
]
|
|
158
|
+
{ email: 'editor@example.com', name: 'Demo Editor', role: 'EDITOR' as const },
|
|
159
|
+
{ email: 'author@example.com', name: 'Demo Author', role: 'AUTHOR' as const },
|
|
160
|
+
]
|
|
172
161
|
|
|
173
162
|
interface SeedOptions {
|
|
174
|
-
demo?: boolean
|
|
175
|
-
file?: string
|
|
176
|
-
reset?: boolean
|
|
163
|
+
demo?: boolean
|
|
164
|
+
file?: string
|
|
165
|
+
reset?: boolean
|
|
177
166
|
}
|
|
178
167
|
|
|
179
168
|
export interface NormalizedSeedDocument {
|
|
180
|
-
collection: string
|
|
181
|
-
data: Record<string, unknown
|
|
182
|
-
status: string
|
|
169
|
+
collection: string
|
|
170
|
+
data: Record<string, unknown>
|
|
171
|
+
status: string
|
|
183
172
|
}
|
|
184
173
|
|
|
185
174
|
export interface NormalizedSeedGlobal {
|
|
186
|
-
slug: string
|
|
187
|
-
data: Record<string, unknown
|
|
175
|
+
slug: string
|
|
176
|
+
data: Record<string, unknown>
|
|
188
177
|
}
|
|
189
178
|
|
|
190
179
|
export interface NormalizedSeedPayload {
|
|
191
|
-
documents: NormalizedSeedDocument[]
|
|
192
|
-
globals: NormalizedSeedGlobal[]
|
|
180
|
+
documents: NormalizedSeedDocument[]
|
|
181
|
+
globals: NormalizedSeedGlobal[]
|
|
193
182
|
}
|
|
194
183
|
|
|
195
184
|
const SEED_FILE_CANDIDATES = [
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
]
|
|
185
|
+
'actuate.seed.json',
|
|
186
|
+
'actuate.seed.ts',
|
|
187
|
+
'actuate.seed.js',
|
|
188
|
+
'actuate.seed.mjs',
|
|
189
|
+
'cms.seed.json',
|
|
190
|
+
]
|
|
202
191
|
|
|
203
192
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
204
|
-
return value && typeof value ===
|
|
205
|
-
? value as Record<string, unknown>
|
|
206
|
-
: {}
|
|
193
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
194
|
+
? (value as Record<string, unknown>)
|
|
195
|
+
: {}
|
|
207
196
|
}
|
|
208
197
|
|
|
209
198
|
function normalizeDocument(collection: string, doc: unknown): NormalizedSeedDocument {
|
|
210
|
-
const record = asRecord(doc)
|
|
199
|
+
const record = asRecord(doc)
|
|
211
200
|
return {
|
|
212
201
|
collection,
|
|
213
202
|
data: asRecord(record.data ?? record),
|
|
214
|
-
status: typeof record.status ===
|
|
215
|
-
}
|
|
203
|
+
status: typeof record.status === 'string' ? record.status : 'DRAFT',
|
|
204
|
+
}
|
|
216
205
|
}
|
|
217
206
|
|
|
218
207
|
export function normalizeSeedPayload(seedData: unknown): NormalizedSeedPayload {
|
|
219
|
-
const documents: NormalizedSeedDocument[] = []
|
|
220
|
-
const globals: NormalizedSeedGlobal[] = []
|
|
208
|
+
const documents: NormalizedSeedDocument[] = []
|
|
209
|
+
const globals: NormalizedSeedGlobal[] = []
|
|
221
210
|
|
|
222
211
|
if (Array.isArray(seedData)) {
|
|
223
212
|
for (const doc of seedData) {
|
|
224
|
-
const record = asRecord(doc)
|
|
225
|
-
documents.push(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
213
|
+
const record = asRecord(doc)
|
|
214
|
+
documents.push(
|
|
215
|
+
normalizeDocument(
|
|
216
|
+
typeof record.collection === 'string' ? record.collection : 'imported',
|
|
217
|
+
record.data ? record : { data: record },
|
|
218
|
+
),
|
|
219
|
+
)
|
|
229
220
|
}
|
|
230
|
-
return { documents, globals }
|
|
221
|
+
return { documents, globals }
|
|
231
222
|
}
|
|
232
223
|
|
|
233
|
-
const root = asRecord(seedData)
|
|
224
|
+
const root = asRecord(seedData)
|
|
234
225
|
|
|
235
|
-
const globalEntries = asRecord(root.globals)
|
|
226
|
+
const globalEntries = asRecord(root.globals)
|
|
236
227
|
for (const [slug, value] of Object.entries(globalEntries)) {
|
|
237
|
-
globals.push({ slug, data: asRecord(value) })
|
|
228
|
+
globals.push({ slug, data: asRecord(value) })
|
|
238
229
|
}
|
|
239
230
|
|
|
240
|
-
const collections = root.collections ? asRecord(root.collections) : root
|
|
231
|
+
const collections = root.collections ? asRecord(root.collections) : root
|
|
241
232
|
for (const [collection, docs] of Object.entries(collections)) {
|
|
242
|
-
if (collection ===
|
|
243
|
-
if (!Array.isArray(docs)) continue
|
|
233
|
+
if (collection === 'globals' || collection === 'collections') continue
|
|
234
|
+
if (!Array.isArray(docs)) continue
|
|
244
235
|
for (const doc of docs) {
|
|
245
|
-
documents.push(normalizeDocument(collection, doc))
|
|
236
|
+
documents.push(normalizeDocument(collection, doc))
|
|
246
237
|
}
|
|
247
238
|
}
|
|
248
239
|
|
|
249
|
-
return { documents, globals }
|
|
240
|
+
return { documents, globals }
|
|
250
241
|
}
|
|
251
242
|
|
|
252
243
|
function findConventionSeedFile(): string | null {
|
|
253
244
|
for (const candidate of SEED_FILE_CANDIDATES) {
|
|
254
|
-
if (existsSync(candidate)) return candidate
|
|
245
|
+
if (existsSync(candidate)) return candidate
|
|
255
246
|
}
|
|
256
|
-
return null
|
|
247
|
+
return null
|
|
257
248
|
}
|
|
258
249
|
|
|
259
250
|
async function loadSeedFile(filePath: string): Promise<unknown> {
|
|
260
|
-
const extension = path.extname(filePath)
|
|
261
|
-
if (extension ===
|
|
262
|
-
const raw = await readFile(filePath,
|
|
263
|
-
return JSON.parse(raw)
|
|
251
|
+
const extension = path.extname(filePath)
|
|
252
|
+
if (extension === '.json' || extension === '') {
|
|
253
|
+
const raw = await readFile(filePath, 'utf-8')
|
|
254
|
+
return JSON.parse(raw)
|
|
264
255
|
}
|
|
265
256
|
|
|
266
|
-
const fileUrl = pathToFileURL(path.resolve(filePath)).href
|
|
267
|
-
const mod =
|
|
268
|
-
|
|
269
|
-
|
|
257
|
+
const fileUrl = pathToFileURL(path.resolve(filePath)).href
|
|
258
|
+
const mod =
|
|
259
|
+
extension === '.ts'
|
|
260
|
+
? await import('tsx/esm/api').then(({ tsImport }) => tsImport(fileUrl, import.meta.url))
|
|
261
|
+
: await import(fileUrl)
|
|
270
262
|
|
|
271
|
-
return (mod as { default?: unknown; seed?: unknown }).default
|
|
272
|
-
?? (mod as { seed?: unknown }).seed;
|
|
263
|
+
return (mod as { default?: unknown; seed?: unknown }).default ?? (mod as { seed?: unknown }).seed
|
|
273
264
|
}
|
|
274
265
|
|
|
275
266
|
async function runSeed(options: SeedOptions): Promise<void> {
|
|
276
|
-
const conventionFile = !options.demo && !options.file ? findConventionSeedFile() : null
|
|
277
|
-
const file = options.file ?? conventionFile ?? undefined
|
|
267
|
+
const conventionFile = !options.demo && !options.file ? findConventionSeedFile() : null
|
|
268
|
+
const file = options.file ?? conventionFile ?? undefined
|
|
278
269
|
|
|
279
270
|
if (!options.demo && !file) {
|
|
280
|
-
logger.error(
|
|
281
|
-
process.exit(1)
|
|
271
|
+
logger.error('Specify --demo, --file <path>, or add actuate.seed.json in the project root.')
|
|
272
|
+
process.exit(1)
|
|
282
273
|
}
|
|
283
274
|
|
|
284
|
-
let seededDb: { db: any; disconnect: () => Promise<void> } | null = null
|
|
275
|
+
let seededDb: { db: any; disconnect: () => Promise<void> } | null = null
|
|
285
276
|
|
|
286
277
|
try {
|
|
287
|
-
seededDb = await getSeedDatabase()
|
|
288
|
-
const db = seededDb.db
|
|
278
|
+
seededDb = await getSeedDatabase()
|
|
279
|
+
const db = seededDb.db
|
|
289
280
|
|
|
290
281
|
if (options.reset) {
|
|
291
|
-
const yes = await confirm(
|
|
292
|
-
"This will delete ALL existing documents and versions. Continue?",
|
|
293
|
-
);
|
|
282
|
+
const yes = await confirm('This will delete ALL existing documents and versions. Continue?')
|
|
294
283
|
if (!yes) {
|
|
295
|
-
logger.warn(
|
|
296
|
-
return
|
|
284
|
+
logger.warn('Seed cancelled.')
|
|
285
|
+
return
|
|
297
286
|
}
|
|
298
287
|
|
|
299
|
-
const resetSpinner = ora(
|
|
300
|
-
await db.version.deleteMany({})
|
|
288
|
+
const resetSpinner = ora('Clearing existing data…').start()
|
|
289
|
+
await db.version.deleteMany({})
|
|
301
290
|
if (db.mediaUsage?.deleteMany) {
|
|
302
|
-
await db.mediaUsage.deleteMany({})
|
|
291
|
+
await db.mediaUsage.deleteMany({})
|
|
303
292
|
}
|
|
304
|
-
await db.document.deleteMany({})
|
|
305
|
-
resetSpinner.succeed(
|
|
293
|
+
await db.document.deleteMany({})
|
|
294
|
+
resetSpinner.succeed('Existing data cleared.')
|
|
306
295
|
}
|
|
307
296
|
|
|
308
297
|
if (options.demo) {
|
|
309
|
-
await seedDemoData(db)
|
|
298
|
+
await seedDemoData(db)
|
|
310
299
|
}
|
|
311
300
|
|
|
312
301
|
if (file) {
|
|
313
|
-
await seedFromFile(db, file)
|
|
302
|
+
await seedFromFile(db, file)
|
|
314
303
|
}
|
|
315
304
|
} catch (err) {
|
|
316
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
317
|
-
logger.error(`Seed failed: ${message}`)
|
|
318
|
-
process.exit(1)
|
|
305
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
306
|
+
logger.error(`Seed failed: ${message}`)
|
|
307
|
+
process.exit(1)
|
|
319
308
|
} finally {
|
|
320
|
-
await seededDb?.disconnect()
|
|
309
|
+
await seededDb?.disconnect()
|
|
321
310
|
}
|
|
322
311
|
}
|
|
323
312
|
|
|
324
313
|
async function getSeedDatabase(): Promise<{ db: any; disconnect: () => Promise<void> }> {
|
|
325
|
-
const { getDB, initDB, isDBInitialized } = await import(
|
|
314
|
+
const { getDB, initDB, isDBInitialized } = await import('@actuate-media/cms-core')
|
|
326
315
|
|
|
327
316
|
if (isDBInitialized()) {
|
|
328
|
-
return { db: getDB<any>(), disconnect: async () => {} }
|
|
317
|
+
return { db: getDB<any>(), disconnect: async () => {} }
|
|
329
318
|
}
|
|
330
319
|
|
|
331
|
-
const db = await createProjectPrismaClient()
|
|
332
|
-
initDB(db)
|
|
320
|
+
const db = await createProjectPrismaClient()
|
|
321
|
+
initDB(db)
|
|
333
322
|
return {
|
|
334
323
|
db,
|
|
335
324
|
disconnect: async () => {
|
|
336
|
-
if (typeof db.$disconnect ===
|
|
337
|
-
await db.$disconnect()
|
|
325
|
+
if (typeof db.$disconnect === 'function') {
|
|
326
|
+
await db.$disconnect()
|
|
338
327
|
}
|
|
339
328
|
},
|
|
340
|
-
}
|
|
329
|
+
}
|
|
341
330
|
}
|
|
342
331
|
|
|
343
332
|
async function createProjectPrismaClient(): Promise<any> {
|
|
344
333
|
if (!process.env.DATABASE_URL) {
|
|
345
|
-
throw new Error(
|
|
334
|
+
throw new Error('DATABASE_URL is required to run seed/populate.')
|
|
346
335
|
}
|
|
347
336
|
|
|
348
|
-
const requireFromProject = createRequire(path.join(process.cwd(),
|
|
349
|
-
const generatedClient = path.resolve(
|
|
337
|
+
const requireFromProject = createRequire(path.join(process.cwd(), 'package.json'))
|
|
338
|
+
const generatedClient = path.resolve('generated', 'prisma', 'client.ts')
|
|
350
339
|
|
|
351
340
|
if (existsSync(generatedClient)) {
|
|
352
341
|
const [{ tsImport }, adapterModule, pgModule] = await Promise.all([
|
|
353
|
-
import(
|
|
354
|
-
import(pathToFileURL(requireFromProject.resolve(
|
|
355
|
-
import(pathToFileURL(requireFromProject.resolve(
|
|
356
|
-
])
|
|
357
|
-
const { PrismaClient } = await tsImport(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
342
|
+
import('tsx/esm/api'),
|
|
343
|
+
import(pathToFileURL(requireFromProject.resolve('@prisma/adapter-pg')).href),
|
|
344
|
+
import(pathToFileURL(requireFromProject.resolve('pg')).href),
|
|
345
|
+
])
|
|
346
|
+
const { PrismaClient } = (await tsImport(
|
|
347
|
+
pathToFileURL(generatedClient).href,
|
|
348
|
+
import.meta.url,
|
|
349
|
+
)) as {
|
|
350
|
+
PrismaClient: new (options?: unknown) => any
|
|
351
|
+
}
|
|
352
|
+
const { PrismaPg } = adapterModule as { PrismaPg: new (pool: unknown) => unknown }
|
|
353
|
+
const pg = (pgModule as { default?: typeof pgModule }).default ?? pgModule
|
|
354
|
+
const pool = new (pg as any).Pool({ connectionString: process.env.DATABASE_URL })
|
|
355
|
+
const adapter = new PrismaPg(pool)
|
|
356
|
+
return new PrismaClient({ adapter } as any)
|
|
365
357
|
}
|
|
366
358
|
|
|
367
|
-
const clientModule = await import(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
359
|
+
const clientModule = (await import(
|
|
360
|
+
pathToFileURL(requireFromProject.resolve('@prisma/client')).href
|
|
361
|
+
)) as {
|
|
362
|
+
PrismaClient: new () => any
|
|
363
|
+
}
|
|
364
|
+
return new clientModule.PrismaClient()
|
|
371
365
|
}
|
|
372
366
|
|
|
373
367
|
export async function ensureSeedAdmin(db: any): Promise<{ id: string }> {
|
|
374
|
-
const existing = await db.user.findFirst({ where: { role:
|
|
375
|
-
if (existing) return existing
|
|
368
|
+
const existing = await db.user.findFirst({ where: { role: 'ADMIN' } })
|
|
369
|
+
if (existing) return existing
|
|
376
370
|
|
|
377
|
-
const email = process.env.CMS_ADMIN_EMAIL
|
|
378
|
-
const password = process.env.CMS_ADMIN_PASSWORD
|
|
379
|
-
const name = process.env.CMS_ADMIN_NAME ??
|
|
371
|
+
const email = process.env.CMS_ADMIN_EMAIL
|
|
372
|
+
const password = process.env.CMS_ADMIN_PASSWORD
|
|
373
|
+
const name = process.env.CMS_ADMIN_NAME ?? 'Admin'
|
|
380
374
|
|
|
381
375
|
if (!email || !password) {
|
|
382
376
|
throw new Error(
|
|
383
|
-
|
|
384
|
-
)
|
|
377
|
+
'No admin user exists. Set CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD before running seed, or complete the setup wizard first.',
|
|
378
|
+
)
|
|
385
379
|
}
|
|
386
380
|
|
|
387
|
-
const { createInitialAdmin } = await import(
|
|
388
|
-
const result = await createInitialAdmin(db, { email, password, name })
|
|
381
|
+
const { createInitialAdmin } = await import('@actuate-media/cms-core')
|
|
382
|
+
const result = await createInitialAdmin(db, { email, password, name })
|
|
389
383
|
if (!result.success || !result.userId) {
|
|
390
|
-
throw new Error(result.error ??
|
|
384
|
+
throw new Error(result.error ?? 'Failed to create initial admin user')
|
|
391
385
|
}
|
|
392
386
|
|
|
393
|
-
return { id: result.userId }
|
|
387
|
+
return { id: result.userId }
|
|
394
388
|
}
|
|
395
389
|
|
|
396
390
|
function sanitizeSeedData(value: unknown, sanitizeHtml: (html: string) => string): unknown {
|
|
397
391
|
if (Array.isArray(value)) {
|
|
398
|
-
return value.map((item) => sanitizeSeedData(item, sanitizeHtml))
|
|
392
|
+
return value.map((item) => sanitizeSeedData(item, sanitizeHtml))
|
|
399
393
|
}
|
|
400
|
-
if (value && typeof value ===
|
|
394
|
+
if (value && typeof value === 'object') {
|
|
401
395
|
return Object.fromEntries(
|
|
402
396
|
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
|
403
397
|
key,
|
|
404
398
|
sanitizeSeedData(item, sanitizeHtml),
|
|
405
399
|
]),
|
|
406
|
-
)
|
|
400
|
+
)
|
|
407
401
|
}
|
|
408
|
-
if (typeof value ===
|
|
409
|
-
return sanitizeHtml(value)
|
|
402
|
+
if (typeof value === 'string' && /<[a-z][\s\S]*>/i.test(value)) {
|
|
403
|
+
return sanitizeHtml(value)
|
|
410
404
|
}
|
|
411
|
-
return value
|
|
405
|
+
return value
|
|
412
406
|
}
|
|
413
407
|
|
|
414
408
|
export async function createSeedDocument(
|
|
@@ -416,78 +410,78 @@ export async function createSeedDocument(
|
|
|
416
410
|
userId: string,
|
|
417
411
|
doc: NormalizedSeedDocument,
|
|
418
412
|
): Promise<void> {
|
|
419
|
-
const { extractPlainText, hashContent, sanitizeHtml } = await import(
|
|
420
|
-
const data = sanitizeSeedData(doc.data, sanitizeHtml) as Record<string, unknown
|
|
421
|
-
const serialized = JSON.stringify(data)
|
|
422
|
-
const plainText = extractPlainText(serialized)
|
|
423
|
-
const contentHash = await hashContent(serialized)
|
|
413
|
+
const { extractPlainText, hashContent, sanitizeHtml } = await import('@actuate-media/cms-core')
|
|
414
|
+
const data = sanitizeSeedData(doc.data, sanitizeHtml) as Record<string, unknown>
|
|
415
|
+
const serialized = JSON.stringify(data)
|
|
416
|
+
const plainText = extractPlainText(serialized)
|
|
417
|
+
const contentHash = await hashContent(serialized)
|
|
424
418
|
|
|
425
419
|
await db.$transaction(async (tx: any) => {
|
|
426
420
|
const created = await tx.document.create({
|
|
427
421
|
data: {
|
|
428
422
|
collection: doc.collection,
|
|
429
|
-
title: typeof data.title ===
|
|
430
|
-
slug: typeof data.slug ===
|
|
423
|
+
title: typeof data.title === 'string' ? data.title : null,
|
|
424
|
+
slug: typeof data.slug === 'string' ? data.slug : null,
|
|
431
425
|
data,
|
|
432
426
|
status: doc.status,
|
|
433
|
-
publishedAt: doc.status ===
|
|
427
|
+
publishedAt: doc.status === 'PUBLISHED' ? new Date() : null,
|
|
434
428
|
createdById: userId,
|
|
435
429
|
updatedById: userId,
|
|
436
430
|
plainText,
|
|
437
431
|
contentHash,
|
|
438
432
|
},
|
|
439
|
-
})
|
|
433
|
+
})
|
|
440
434
|
|
|
441
435
|
await tx.version.create({
|
|
442
436
|
data: {
|
|
443
437
|
documentId: created.id,
|
|
444
438
|
data,
|
|
445
439
|
changedById: userId,
|
|
446
|
-
changeType:
|
|
440
|
+
changeType: 'CREATE',
|
|
447
441
|
},
|
|
448
|
-
})
|
|
449
|
-
})
|
|
442
|
+
})
|
|
443
|
+
})
|
|
450
444
|
}
|
|
451
445
|
|
|
452
446
|
async function seedDemoData(db: any): Promise<void> {
|
|
453
|
-
const spinner = ora(
|
|
447
|
+
const spinner = ora('Seeding demo data…').start()
|
|
454
448
|
|
|
455
|
-
const adminUser = await ensureSeedAdmin(db)
|
|
456
|
-
const userId = adminUser.id
|
|
449
|
+
const adminUser = await ensureSeedAdmin(db)
|
|
450
|
+
const userId = adminUser.id
|
|
457
451
|
|
|
458
|
-
let pagesCreated = 0
|
|
452
|
+
let pagesCreated = 0
|
|
459
453
|
for (const page of DEMO_PAGES) {
|
|
460
454
|
await createSeedDocument(db, userId, {
|
|
461
|
-
collection:
|
|
455
|
+
collection: 'pages',
|
|
462
456
|
data: page,
|
|
463
|
-
status:
|
|
464
|
-
})
|
|
465
|
-
pagesCreated
|
|
457
|
+
status: 'PUBLISHED',
|
|
458
|
+
})
|
|
459
|
+
pagesCreated++
|
|
466
460
|
}
|
|
467
461
|
|
|
468
|
-
let postsCreated = 0
|
|
462
|
+
let postsCreated = 0
|
|
469
463
|
for (const post of DEMO_POSTS) {
|
|
470
464
|
await createSeedDocument(db, userId, {
|
|
471
|
-
collection:
|
|
465
|
+
collection: 'posts',
|
|
472
466
|
data: post,
|
|
473
467
|
status: post.status,
|
|
474
|
-
})
|
|
475
|
-
postsCreated
|
|
468
|
+
})
|
|
469
|
+
postsCreated++
|
|
476
470
|
}
|
|
477
471
|
|
|
478
|
-
let formsCreated = 0
|
|
472
|
+
let formsCreated = 0
|
|
479
473
|
for (const form of DEMO_FORMS) {
|
|
480
474
|
await createSeedDocument(db, userId, {
|
|
481
|
-
collection:
|
|
475
|
+
collection: 'forms',
|
|
482
476
|
data: form,
|
|
483
|
-
status:
|
|
484
|
-
})
|
|
485
|
-
formsCreated
|
|
477
|
+
status: 'PUBLISHED',
|
|
478
|
+
})
|
|
479
|
+
formsCreated++
|
|
486
480
|
}
|
|
487
481
|
|
|
488
|
-
let usersCreated = 0
|
|
482
|
+
let usersCreated = 0
|
|
489
483
|
for (const user of DEMO_USERS) {
|
|
490
|
-
const exists = await db.user.findFirst({ where: { email: user.email } })
|
|
484
|
+
const exists = await db.user.findFirst({ where: { email: user.email } })
|
|
491
485
|
if (!exists) {
|
|
492
486
|
await db.user.create({
|
|
493
487
|
data: {
|
|
@@ -498,74 +492,74 @@ async function seedDemoData(db: any): Promise<void> {
|
|
|
498
492
|
isApproved: true,
|
|
499
493
|
emailVerified: true,
|
|
500
494
|
},
|
|
501
|
-
})
|
|
502
|
-
usersCreated
|
|
495
|
+
})
|
|
496
|
+
usersCreated++
|
|
503
497
|
}
|
|
504
498
|
}
|
|
505
499
|
|
|
506
|
-
spinner.succeed(
|
|
507
|
-
logger.info(` Pages: ${pagesCreated}`)
|
|
508
|
-
logger.info(` Posts: ${postsCreated}`)
|
|
509
|
-
logger.info(` Forms: ${formsCreated}`)
|
|
510
|
-
logger.info(` Users: ${usersCreated} (+ existing admin)`)
|
|
500
|
+
spinner.succeed('Demo data seeded successfully.')
|
|
501
|
+
logger.info(` Pages: ${pagesCreated}`)
|
|
502
|
+
logger.info(` Posts: ${postsCreated}`)
|
|
503
|
+
logger.info(` Forms: ${formsCreated}`)
|
|
504
|
+
logger.info(` Users: ${usersCreated} (+ existing admin)`)
|
|
511
505
|
}
|
|
512
506
|
|
|
513
507
|
async function seedFromFile(db: any, filePath: string): Promise<void> {
|
|
514
508
|
if (!existsSync(filePath)) {
|
|
515
|
-
logger.error(`File not found: ${filePath}`)
|
|
516
|
-
process.exit(1)
|
|
509
|
+
logger.error(`File not found: ${filePath}`)
|
|
510
|
+
process.exit(1)
|
|
517
511
|
}
|
|
518
512
|
|
|
519
|
-
const spinner = ora(`Seeding from ${filePath}…`).start()
|
|
513
|
+
const spinner = ora(`Seeding from ${filePath}…`).start()
|
|
520
514
|
|
|
521
|
-
let seedData: any
|
|
515
|
+
let seedData: any
|
|
522
516
|
try {
|
|
523
|
-
seedData = await loadSeedFile(filePath)
|
|
517
|
+
seedData = await loadSeedFile(filePath)
|
|
524
518
|
} catch {
|
|
525
|
-
spinner.fail(
|
|
526
|
-
process.exit(1)
|
|
519
|
+
spinner.fail('Invalid seed file.')
|
|
520
|
+
process.exit(1)
|
|
527
521
|
}
|
|
528
522
|
|
|
529
|
-
if (!Array.isArray(seedData) && typeof seedData !==
|
|
530
|
-
spinner.fail(
|
|
531
|
-
process.exit(1)
|
|
523
|
+
if (!Array.isArray(seedData) && typeof seedData !== 'object') {
|
|
524
|
+
spinner.fail('Seed file must contain a JSON array or an object with collection keys.')
|
|
525
|
+
process.exit(1)
|
|
532
526
|
}
|
|
533
527
|
|
|
534
|
-
const adminUser = await ensureSeedAdmin(db)
|
|
535
|
-
const userId = adminUser.id
|
|
528
|
+
const adminUser = await ensureSeedAdmin(db)
|
|
529
|
+
const userId = adminUser.id
|
|
536
530
|
|
|
537
|
-
const normalized = normalizeSeedPayload(seedData)
|
|
538
|
-
const { updateGlobal } = await import(
|
|
539
|
-
const ctx = { userId, role:
|
|
531
|
+
const normalized = normalizeSeedPayload(seedData)
|
|
532
|
+
const { updateGlobal } = await import('@actuate-media/cms-core')
|
|
533
|
+
const ctx = { userId, role: 'ADMIN', db }
|
|
540
534
|
|
|
541
|
-
let documentCount = 0
|
|
535
|
+
let documentCount = 0
|
|
542
536
|
for (const doc of normalized.documents) {
|
|
543
|
-
await createSeedDocument(db, userId, doc)
|
|
544
|
-
documentCount
|
|
537
|
+
await createSeedDocument(db, userId, doc)
|
|
538
|
+
documentCount++
|
|
545
539
|
}
|
|
546
540
|
|
|
547
|
-
let globalCount = 0
|
|
541
|
+
let globalCount = 0
|
|
548
542
|
for (const global of normalized.globals) {
|
|
549
|
-
await updateGlobal(global.slug, global.data, ctx)
|
|
550
|
-
globalCount
|
|
543
|
+
await updateGlobal(global.slug, global.data, ctx)
|
|
544
|
+
globalCount++
|
|
551
545
|
}
|
|
552
546
|
|
|
553
|
-
spinner.succeed(`Seeded ${documentCount} documents and ${globalCount} globals from ${filePath}.`)
|
|
547
|
+
spinner.succeed(`Seeded ${documentCount} documents and ${globalCount} globals from ${filePath}.`)
|
|
554
548
|
}
|
|
555
549
|
|
|
556
550
|
export function registerSeedCommand(program: Command): void {
|
|
557
551
|
program
|
|
558
|
-
.command(
|
|
559
|
-
.description(
|
|
560
|
-
.option(
|
|
561
|
-
.option(
|
|
562
|
-
.option(
|
|
563
|
-
.action(runSeed)
|
|
552
|
+
.command('seed')
|
|
553
|
+
.description('Seed the database with demo or custom data')
|
|
554
|
+
.option('--demo', 'Seed demo content (pages, posts, forms, users)')
|
|
555
|
+
.option('--file <path>', 'Seed from a JSON, JavaScript, or TypeScript file')
|
|
556
|
+
.option('--reset', 'Clear existing data before seeding')
|
|
557
|
+
.action(runSeed)
|
|
564
558
|
|
|
565
559
|
program
|
|
566
|
-
.command(
|
|
567
|
-
.description(
|
|
568
|
-
.option(
|
|
569
|
-
.option(
|
|
570
|
-
.action(runSeed)
|
|
560
|
+
.command('populate')
|
|
561
|
+
.description('Populate the database from actuate.seed.json or a custom seed file')
|
|
562
|
+
.option('--file <path>', 'Seed from a JSON, JavaScript, or TypeScript file')
|
|
563
|
+
.option('--reset', 'Clear existing data before seeding')
|
|
564
|
+
.action(runSeed)
|
|
571
565
|
}
|