@actuate-media/cli 0.3.0 → 0.4.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 +16 -7
- package/CHANGELOG.md +12 -0
- package/dist/__tests__/deployment-diagnostics.test.d.ts +2 -0
- package/dist/__tests__/deployment-diagnostics.test.d.ts.map +1 -0
- package/dist/__tests__/deployment-diagnostics.test.js +76 -0
- package/dist/__tests__/deployment-diagnostics.test.js.map +1 -0
- package/dist/__tests__/schema-fragment.test.d.ts +2 -0
- package/dist/__tests__/schema-fragment.test.d.ts.map +1 -0
- package/dist/__tests__/schema-fragment.test.js +40 -0
- package/dist/__tests__/schema-fragment.test.js.map +1 -0
- package/dist/__tests__/seed.test.js +68 -2
- package/dist/__tests__/seed.test.js.map +1 -1
- package/dist/commands/db-init.d.ts +4 -0
- package/dist/commands/db-init.d.ts.map +1 -1
- package/dist/commands/db-init.js +81 -2
- package/dist/commands/db-init.js.map +1 -1
- package/dist/commands/db-status.d.ts.map +1 -1
- package/dist/commands/db-status.js +2 -12
- package/dist/commands/db-status.js.map +1 -1
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +153 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/seed.d.ts +4 -0
- package/dist/commands/seed.d.ts.map +1 -1
- package/dist/commands/seed.js +126 -86
- package/dist/commands/seed.js.map +1 -1
- package/dist/deployment/diagnostics.d.ts +58 -0
- package/dist/deployment/diagnostics.d.ts.map +1 -0
- package/dist/deployment/diagnostics.js +154 -0
- package/dist/deployment/diagnostics.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/deployment-diagnostics.test.ts +88 -0
- package/src/__tests__/schema-fragment.test.ts +47 -0
- package/src/__tests__/seed.test.ts +75 -2
- package/src/commands/db-init.ts +85 -2
- package/src/commands/db-status.ts +2 -13
- package/src/commands/doctor.ts +159 -0
- package/src/commands/seed.ts +153 -88
- package/src/deployment/diagnostics.ts +189 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export const REQUIRED_CMS_MODELS = [
|
|
2
|
+
'User',
|
|
3
|
+
'Session',
|
|
4
|
+
'Document',
|
|
5
|
+
'Media',
|
|
6
|
+
'Version',
|
|
7
|
+
'Folder',
|
|
8
|
+
'Redirect',
|
|
9
|
+
'FormSubmission',
|
|
10
|
+
'AuditLog',
|
|
11
|
+
'PasswordResetToken',
|
|
12
|
+
'MediaUsage',
|
|
13
|
+
'ScriptTag',
|
|
14
|
+
'PageTemplate',
|
|
15
|
+
'SavedSection',
|
|
16
|
+
];
|
|
17
|
+
export const REQUIRED_ENV_VARS = [
|
|
18
|
+
'DATABASE_URL',
|
|
19
|
+
'CMS_SECRET',
|
|
20
|
+
'CMS_ENCRYPTION_KEY',
|
|
21
|
+
'NEXT_PUBLIC_SITE_URL',
|
|
22
|
+
];
|
|
23
|
+
export function missingModels(schemaModels) {
|
|
24
|
+
return REQUIRED_CMS_MODELS.filter((model) => !schemaModels.has(model));
|
|
25
|
+
}
|
|
26
|
+
export function missingEnvVars(env) {
|
|
27
|
+
return REQUIRED_ENV_VARS.filter((name) => !env[name]);
|
|
28
|
+
}
|
|
29
|
+
export function detectPackageManager(lockfiles) {
|
|
30
|
+
if (lockfiles.has('pnpm-lock.yaml'))
|
|
31
|
+
return 'pnpm';
|
|
32
|
+
if (lockfiles.has('yarn.lock'))
|
|
33
|
+
return 'yarn';
|
|
34
|
+
if (lockfiles.has('package-lock.json'))
|
|
35
|
+
return 'npm';
|
|
36
|
+
return 'npm';
|
|
37
|
+
}
|
|
38
|
+
export function createDiagnosticReport(input) {
|
|
39
|
+
const checks = [];
|
|
40
|
+
const models = missingModels(input.schemaModels);
|
|
41
|
+
const envVars = input.mode === 'deploy'
|
|
42
|
+
? [...missingEnvVars(input.env), ...(!input.env.DIRECT_DATABASE_URL ? ['DIRECT_DATABASE_URL'] : [])]
|
|
43
|
+
: missingEnvVars(input.env);
|
|
44
|
+
const fieldProblems = input.schemaContent ? missingCriticalFields(input.schemaContent) : [];
|
|
45
|
+
checks.push({
|
|
46
|
+
id: 'schema-models',
|
|
47
|
+
label: 'Prisma schema models',
|
|
48
|
+
status: models.length === 0 ? 'pass' : 'fail',
|
|
49
|
+
message: models.length === 0
|
|
50
|
+
? 'All deploy-critical Actuate models are present.'
|
|
51
|
+
: `Missing deploy-critical Actuate models: ${models.join(', ')}.`,
|
|
52
|
+
fix: models.length === 0
|
|
53
|
+
? undefined
|
|
54
|
+
: `Run \`actuate db:init --schema ${input.schemaPath}\` for new schemas, or update the existing Actuate block from the database setup docs, then create and apply a Prisma migration.`,
|
|
55
|
+
docs: 'https://actuatecms.dev/docs/database-setup',
|
|
56
|
+
});
|
|
57
|
+
checks.push({
|
|
58
|
+
id: 'schema-fields',
|
|
59
|
+
label: 'Prisma schema fields',
|
|
60
|
+
status: fieldProblems.length === 0 ? 'pass' : 'fail',
|
|
61
|
+
message: fieldProblems.length === 0
|
|
62
|
+
? 'Deploy-critical model fields are present.'
|
|
63
|
+
: `Missing deploy-critical model fields: ${fieldProblems.join(', ')}.`,
|
|
64
|
+
fix: fieldProblems.length === 0
|
|
65
|
+
? undefined
|
|
66
|
+
: 'Update the Actuate Prisma models from the database setup docs, then create and apply a Prisma migration.',
|
|
67
|
+
docs: 'https://actuatecms.dev/docs/database-setup',
|
|
68
|
+
});
|
|
69
|
+
checks.push({
|
|
70
|
+
id: 'environment',
|
|
71
|
+
label: 'Required environment variables',
|
|
72
|
+
status: envVars.length === 0 ? 'pass' : 'fail',
|
|
73
|
+
message: envVars.length === 0
|
|
74
|
+
? 'Required deployment environment variables are set.'
|
|
75
|
+
: `Missing required environment variables: ${envVars.join(', ')}.`,
|
|
76
|
+
fix: envVars.length === 0
|
|
77
|
+
? undefined
|
|
78
|
+
: `Set missing environment variables before deploying: ${envVars.join(', ')}.`,
|
|
79
|
+
docs: 'https://actuatecms.dev/docs/environment-variables',
|
|
80
|
+
});
|
|
81
|
+
checks.push({
|
|
82
|
+
id: 'package-manager',
|
|
83
|
+
label: 'Package manager',
|
|
84
|
+
status: 'pass',
|
|
85
|
+
message: `Detected ${input.packageManager}. Use the same package manager for install, build, and deploy checks.`,
|
|
86
|
+
});
|
|
87
|
+
const status = checks.some((check) => check.status === 'fail')
|
|
88
|
+
? 'fail'
|
|
89
|
+
: checks.some((check) => check.status === 'warn')
|
|
90
|
+
? 'warn'
|
|
91
|
+
: 'pass';
|
|
92
|
+
return { status, checks };
|
|
93
|
+
}
|
|
94
|
+
export function buildDeploymentManifest() {
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
packageScope: '@actuate-media',
|
|
98
|
+
registry: 'https://registry.npmjs.org',
|
|
99
|
+
requiredModels: [...REQUIRED_CMS_MODELS],
|
|
100
|
+
requiredEnv: [...REQUIRED_ENV_VARS],
|
|
101
|
+
optionalEnv: [
|
|
102
|
+
'DIRECT_DATABASE_URL',
|
|
103
|
+
'CMS_ADMIN_EMAIL',
|
|
104
|
+
'CMS_ADMIN_PASSWORD',
|
|
105
|
+
'BLOB_READ_WRITE_TOKEN',
|
|
106
|
+
'UPSTASH_REDIS_REST_URL',
|
|
107
|
+
'UPSTASH_REDIS_REST_TOKEN',
|
|
108
|
+
'RESEND_API_KEY',
|
|
109
|
+
],
|
|
110
|
+
routes: {
|
|
111
|
+
apiHealth: '/api/cms/health',
|
|
112
|
+
apiBase: '/api/cms',
|
|
113
|
+
adminDefault: '/admin',
|
|
114
|
+
},
|
|
115
|
+
crons: {
|
|
116
|
+
scheduledPublish: '/api/cms/cron/publish',
|
|
117
|
+
cleanup: '/api/cms/cron/cleanup',
|
|
118
|
+
seoScan: '/api/cms/cron/seo-scan',
|
|
119
|
+
},
|
|
120
|
+
verification: {
|
|
121
|
+
preflight: 'actuate doctor',
|
|
122
|
+
production: 'actuate deploy:check',
|
|
123
|
+
postDeploy: 'actuate verify --full',
|
|
124
|
+
},
|
|
125
|
+
docs: {
|
|
126
|
+
deployment: 'https://actuatecms.dev/docs/deployment',
|
|
127
|
+
databaseSetup: 'https://actuatecms.dev/docs/database-setup',
|
|
128
|
+
environmentVariables: 'https://actuatecms.dev/docs/environment-variables',
|
|
129
|
+
aiRunbook: 'https://actuatecms.dev/docs/ai-runbook',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function missingCriticalFields(schemaContent) {
|
|
134
|
+
const requiredFields = {
|
|
135
|
+
ScriptTag: ['targetPaths', 'priority', 'enabled'],
|
|
136
|
+
PageTemplate: ['tree', 'builtIn', 'category'],
|
|
137
|
+
SavedSection: ['tree', 'usageCount', 'category'],
|
|
138
|
+
PasswordResetToken: ['tokenHash', 'expiresAt', 'usedAt'],
|
|
139
|
+
MediaUsage: ['mediaId', 'documentId', 'fieldPath'],
|
|
140
|
+
};
|
|
141
|
+
const missing = [];
|
|
142
|
+
for (const [model, fields] of Object.entries(requiredFields)) {
|
|
143
|
+
const body = schemaContent.match(new RegExp(`model\\s+${model}\\s+\\{([\\s\\S]*?)\\n\\}`))?.[1] ?? '';
|
|
144
|
+
if (!body)
|
|
145
|
+
continue;
|
|
146
|
+
for (const field of fields) {
|
|
147
|
+
if (!new RegExp(`^\\s*${field}\\s+`, 'm').test(body)) {
|
|
148
|
+
missing.push(`${model}.${field}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return missing;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/deployment/diagnostics.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerUpdateCheckCommand } from "./commands/update-check.js";
|
|
|
10
10
|
import { registerDbInitCommand } from "./commands/db-init.js";
|
|
11
11
|
import { registerDbStatusCommand } from "./commands/db-status.js";
|
|
12
12
|
import { registerInitCommand } from "./commands/init.js";
|
|
13
|
+
import { registerDeployCheckCommand, registerDoctorCommand, registerVerifyCommand, } from "./commands/doctor.js";
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program
|
|
15
16
|
.name("actuate")
|
|
@@ -25,5 +26,8 @@ registerUpdateCheckCommand(program);
|
|
|
25
26
|
registerDbInitCommand(program);
|
|
26
27
|
registerDbStatusCommand(program);
|
|
27
28
|
registerInitCommand(program);
|
|
29
|
+
registerDoctorCommand(program);
|
|
30
|
+
registerDeployCheckCommand(program);
|
|
31
|
+
registerVerifyCommand(program);
|
|
28
32
|
program.parse();
|
|
29
33
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AACjC,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,0BAA0B,CAAC,OAAO,CAAC,CAAC;AACpC,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,uBAAuB,CAAC,OAAO,CAAC,CAAC;AACjC,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,0BAA0B,CAAC,OAAO,CAAC,CAAC;AACpC,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAE/B,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for Actuate CMS — migrations, codegen, imports, exports, and upgrades",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"typescript": "^5.7.0",
|
|
30
30
|
"vitest": "^3.0.0",
|
|
31
|
-
"@actuate-media/cms-core": "0.10.
|
|
31
|
+
"@actuate-media/cms-core": "0.10.3"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildDeploymentManifest,
|
|
5
|
+
createDiagnosticReport,
|
|
6
|
+
REQUIRED_CMS_MODELS,
|
|
7
|
+
REQUIRED_ENV_VARS,
|
|
8
|
+
} from '../deployment/diagnostics.js';
|
|
9
|
+
|
|
10
|
+
describe('deployment diagnostics', () => {
|
|
11
|
+
it('tracks deploy-critical models used by first-run admin features', () => {
|
|
12
|
+
expect(REQUIRED_CMS_MODELS).toEqual(expect.arrayContaining([
|
|
13
|
+
'User',
|
|
14
|
+
'Session',
|
|
15
|
+
'Document',
|
|
16
|
+
'Media',
|
|
17
|
+
'Version',
|
|
18
|
+
'Folder',
|
|
19
|
+
'Redirect',
|
|
20
|
+
'FormSubmission',
|
|
21
|
+
'AuditLog',
|
|
22
|
+
'PasswordResetToken',
|
|
23
|
+
'MediaUsage',
|
|
24
|
+
'ScriptTag',
|
|
25
|
+
'PageTemplate',
|
|
26
|
+
'SavedSection',
|
|
27
|
+
]));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('reports missing models and env vars with exact fix commands', () => {
|
|
31
|
+
const report = createDiagnosticReport({
|
|
32
|
+
schemaModels: new Set(['User', 'Session', 'Document']),
|
|
33
|
+
schemaContent: '',
|
|
34
|
+
env: {
|
|
35
|
+
DATABASE_URL: 'postgresql://example',
|
|
36
|
+
},
|
|
37
|
+
packageManager: 'pnpm',
|
|
38
|
+
schemaPath: 'prisma/schema.prisma',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(report.status).toBe('fail');
|
|
42
|
+
expect(report.checks).toEqual(expect.arrayContaining([
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
id: 'schema-models',
|
|
45
|
+
status: 'fail',
|
|
46
|
+
fix: 'Run `actuate db:init --schema prisma/schema.prisma` for new schemas, or update the existing Actuate block from the database setup docs, then create and apply a Prisma migration.',
|
|
47
|
+
}),
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
id: 'environment',
|
|
50
|
+
status: 'fail',
|
|
51
|
+
fix: 'Set missing environment variables before deploying: CMS_SECRET, CMS_ENCRYPTION_KEY, NEXT_PUBLIC_SITE_URL.',
|
|
52
|
+
}),
|
|
53
|
+
]));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('requires migration connection details for deploy checks', () => {
|
|
57
|
+
const report = createDiagnosticReport({
|
|
58
|
+
schemaModels: new Set(REQUIRED_CMS_MODELS),
|
|
59
|
+
schemaContent: '',
|
|
60
|
+
env: {
|
|
61
|
+
DATABASE_URL: 'postgresql://example',
|
|
62
|
+
CMS_SECRET: 'secret',
|
|
63
|
+
CMS_ENCRYPTION_KEY: 'a'.repeat(64),
|
|
64
|
+
NEXT_PUBLIC_SITE_URL: 'https://example.com',
|
|
65
|
+
},
|
|
66
|
+
packageManager: 'pnpm',
|
|
67
|
+
schemaPath: 'prisma/schema.prisma',
|
|
68
|
+
mode: 'deploy',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(report.status).toBe('fail');
|
|
72
|
+
expect(report.checks).toEqual(expect.arrayContaining([
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
id: 'environment',
|
|
75
|
+
message: expect.stringContaining('DIRECT_DATABASE_URL'),
|
|
76
|
+
}),
|
|
77
|
+
]));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('exposes machine-readable deployment metadata', () => {
|
|
81
|
+
const manifest = buildDeploymentManifest();
|
|
82
|
+
|
|
83
|
+
expect(manifest.requiredModels).toEqual(REQUIRED_CMS_MODELS);
|
|
84
|
+
expect(manifest.requiredEnv).toEqual(REQUIRED_ENV_VARS);
|
|
85
|
+
expect(manifest.routes.apiHealth).toBe('/api/cms/health');
|
|
86
|
+
expect(manifest.crons.scheduledPublish).toBe('/api/cms/cron/publish');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { registerDbInitCommand, resetDbInitCommandRunner, setDbInitCommandRunner } from '../commands/db-init.js';
|
|
8
|
+
import { REQUIRED_CMS_MODELS } from '../deployment/diagnostics.js';
|
|
9
|
+
|
|
10
|
+
function baseSchema() {
|
|
11
|
+
return `generator client {
|
|
12
|
+
provider = "prisma-client-js"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
datasource db {
|
|
16
|
+
provider = "postgresql"
|
|
17
|
+
url = env("DATABASE_URL")
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('db:init schema fragment', () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
resetDbInitCommandRunner();
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('injects all deploy-critical Actuate models', async () => {
|
|
29
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), 'actuate-db-init-'));
|
|
30
|
+
const schemaPath = path.join(tempRoot, 'schema.prisma');
|
|
31
|
+
await writeFile(schemaPath, baseSchema());
|
|
32
|
+
|
|
33
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempRoot);
|
|
34
|
+
setDbInitCommandRunner(() => undefined);
|
|
35
|
+
const command = new Command();
|
|
36
|
+
registerDbInitCommand(command);
|
|
37
|
+
|
|
38
|
+
await command.parseAsync(['node', 'actuate', 'db:init', '--schema', 'schema.prisma']);
|
|
39
|
+
|
|
40
|
+
const schema = await readFile(schemaPath, 'utf-8');
|
|
41
|
+
for (const model of REQUIRED_CMS_MODELS) {
|
|
42
|
+
expect(schema).toContain(`model ${model} `);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
vi.mock('@actuate-media/cms-core', () => ({
|
|
4
|
+
extractPlainText: vi.fn((value: string) => value.replace(/<[^>]+>/g, ' ')),
|
|
5
|
+
hashContent: vi.fn(async () => 'hash-1'),
|
|
6
|
+
sanitizeHtml: vi.fn((value: string) => value.replace(/<script[\s\S]*?<\/script>/gi, '')),
|
|
7
|
+
createInitialAdmin: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { createSeedDocument, ensureSeedAdmin, normalizeSeedPayload } from '../commands/seed.js';
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.unstubAllEnvs();
|
|
14
|
+
});
|
|
4
15
|
|
|
5
16
|
describe('seed payload normalization', () => {
|
|
6
17
|
it('separates globals from collection documents', () => {
|
|
@@ -51,3 +62,65 @@ describe('seed payload normalization', () => {
|
|
|
51
62
|
]);
|
|
52
63
|
});
|
|
53
64
|
});
|
|
65
|
+
|
|
66
|
+
describe('seed admin handling', () => {
|
|
67
|
+
it('refuses to create a first admin without credentials', async () => {
|
|
68
|
+
const db = {
|
|
69
|
+
user: {
|
|
70
|
+
findFirst: vi.fn().mockResolvedValue(null),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await expect(ensureSeedAdmin(db)).rejects.toThrow('CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('seed document creation', () => {
|
|
79
|
+
it('creates documents with versions, content hashes, and sanitized HTML', async () => {
|
|
80
|
+
const tx = {
|
|
81
|
+
document: {
|
|
82
|
+
create: vi.fn().mockResolvedValue({ id: 'doc-1' }),
|
|
83
|
+
},
|
|
84
|
+
version: {
|
|
85
|
+
create: vi.fn(),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const db = {
|
|
89
|
+
$transaction: vi.fn(async (fn) => fn(tx)),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await createSeedDocument(db, 'admin-1', {
|
|
93
|
+
collection: 'pages',
|
|
94
|
+
status: 'PUBLISHED',
|
|
95
|
+
data: {
|
|
96
|
+
title: 'Home',
|
|
97
|
+
slug: 'home',
|
|
98
|
+
content: '<p>Hello</p><script>alert(1)</script>',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(tx.document.create).toHaveBeenCalledWith({
|
|
103
|
+
data: expect.objectContaining({
|
|
104
|
+
collection: 'pages',
|
|
105
|
+
title: 'Home',
|
|
106
|
+
slug: 'home',
|
|
107
|
+
status: 'PUBLISHED',
|
|
108
|
+
publishedAt: expect.any(Date),
|
|
109
|
+
createdById: 'admin-1',
|
|
110
|
+
updatedById: 'admin-1',
|
|
111
|
+
plainText: expect.any(String),
|
|
112
|
+
contentHash: expect.any(String),
|
|
113
|
+
data: expect.objectContaining({
|
|
114
|
+
content: '<p>Hello</p>',
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
expect(tx.version.create).toHaveBeenCalledWith({
|
|
119
|
+
data: expect.objectContaining({
|
|
120
|
+
documentId: 'doc-1',
|
|
121
|
+
changedById: 'admin-1',
|
|
122
|
+
changeType: 'CREATE',
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/commands/db-init.ts
CHANGED
|
@@ -7,6 +7,20 @@ import { logger } from "../utils/logger.js";
|
|
|
7
7
|
|
|
8
8
|
const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
|
|
9
9
|
|
|
10
|
+
export let runDbInitCommand = (command: string, options: ExecSyncOptions): void => {
|
|
11
|
+
execSync(command, options);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const defaultDbInitCommandRunner = runDbInitCommand;
|
|
15
|
+
|
|
16
|
+
export function setDbInitCommandRunner(runner: typeof runDbInitCommand): void {
|
|
17
|
+
runDbInitCommand = runner;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resetDbInitCommandRunner(): void {
|
|
21
|
+
runDbInitCommand = defaultDbInitCommandRunner;
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
11
25
|
try {
|
|
12
26
|
await access(filePath);
|
|
@@ -219,6 +233,75 @@ model AuditLog {
|
|
|
219
233
|
@@index([userId])
|
|
220
234
|
@@index([createdAt])
|
|
221
235
|
}
|
|
236
|
+
|
|
237
|
+
model PasswordResetToken {
|
|
238
|
+
id String @id @default(cuid())
|
|
239
|
+
userId String
|
|
240
|
+
tokenHash String
|
|
241
|
+
expiresAt DateTime
|
|
242
|
+
usedAt DateTime?
|
|
243
|
+
createdAt DateTime @default(now())
|
|
244
|
+
|
|
245
|
+
@@index([tokenHash])
|
|
246
|
+
@@index([userId])
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
model MediaUsage {
|
|
250
|
+
id String @id @default(cuid())
|
|
251
|
+
mediaId String
|
|
252
|
+
documentId String
|
|
253
|
+
fieldPath String?
|
|
254
|
+
createdAt DateTime @default(now())
|
|
255
|
+
|
|
256
|
+
@@unique([mediaId, documentId, fieldPath])
|
|
257
|
+
@@index([mediaId])
|
|
258
|
+
@@index([documentId])
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
model ScriptTag {
|
|
262
|
+
id String @id @default(cuid())
|
|
263
|
+
name String
|
|
264
|
+
code String @db.Text
|
|
265
|
+
placement String
|
|
266
|
+
scope String
|
|
267
|
+
targetPaths String[]
|
|
268
|
+
priority Int @default(100)
|
|
269
|
+
enabled Boolean @default(true)
|
|
270
|
+
createdAt DateTime @default(now())
|
|
271
|
+
updatedAt DateTime @updatedAt
|
|
272
|
+
|
|
273
|
+
@@index([enabled])
|
|
274
|
+
@@index([placement])
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
model PageTemplate {
|
|
278
|
+
id String @id @default(cuid())
|
|
279
|
+
name String
|
|
280
|
+
description String?
|
|
281
|
+
category String @default("content")
|
|
282
|
+
tree Json
|
|
283
|
+
thumbnail String?
|
|
284
|
+
builtIn Boolean @default(false)
|
|
285
|
+
createdAt DateTime @default(now())
|
|
286
|
+
updatedAt DateTime @updatedAt
|
|
287
|
+
|
|
288
|
+
@@index([category])
|
|
289
|
+
@@index([builtIn])
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
model SavedSection {
|
|
293
|
+
id String @id @default(cuid())
|
|
294
|
+
name String
|
|
295
|
+
description String?
|
|
296
|
+
category String @default("content")
|
|
297
|
+
tree Json
|
|
298
|
+
thumbnail String?
|
|
299
|
+
usageCount Int @default(0)
|
|
300
|
+
createdAt DateTime @default(now())
|
|
301
|
+
updatedAt DateTime @updatedAt
|
|
302
|
+
|
|
303
|
+
@@index([category])
|
|
304
|
+
}
|
|
222
305
|
`;
|
|
223
306
|
}
|
|
224
307
|
|
|
@@ -279,7 +362,7 @@ export function registerDbInitCommand(program: Command): void {
|
|
|
279
362
|
const genSpinner = ora("Running prisma generate...").start();
|
|
280
363
|
try {
|
|
281
364
|
genSpinner.stop();
|
|
282
|
-
|
|
365
|
+
runDbInitCommand("npx prisma generate", execOpts);
|
|
283
366
|
logger.success("Prisma client generated.");
|
|
284
367
|
} catch {
|
|
285
368
|
logger.warn("prisma generate failed. You may need to set DATABASE_URL first.");
|
|
@@ -289,7 +372,7 @@ export function registerDbInitCommand(program: Command): void {
|
|
|
289
372
|
const migSpinner = ora("Running prisma migrate dev...").start();
|
|
290
373
|
try {
|
|
291
374
|
migSpinner.stop();
|
|
292
|
-
|
|
375
|
+
runDbInitCommand("npx prisma migrate dev --name actuate-cms-init", execOpts);
|
|
293
376
|
logger.success("Migration created and applied.");
|
|
294
377
|
} catch {
|
|
295
378
|
logger.warn("prisma migrate dev failed. Run it manually after setting DATABASE_URL.");
|
|
@@ -5,18 +5,7 @@ import { resolve } from "node:path";
|
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
-
|
|
9
|
-
const CMS_EXPECTED_MODELS = [
|
|
10
|
-
"User",
|
|
11
|
-
"Session",
|
|
12
|
-
"Document",
|
|
13
|
-
"Media",
|
|
14
|
-
"Version",
|
|
15
|
-
"Folder",
|
|
16
|
-
"Redirect",
|
|
17
|
-
"FormSubmission",
|
|
18
|
-
"AuditLog",
|
|
19
|
-
];
|
|
8
|
+
import { REQUIRED_CMS_MODELS } from "../deployment/diagnostics.js";
|
|
20
9
|
|
|
21
10
|
const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
|
|
22
11
|
|
|
@@ -74,7 +63,7 @@ export function registerDbStatusCommand(program: Command): void {
|
|
|
74
63
|
let present = 0;
|
|
75
64
|
let missing = 0;
|
|
76
65
|
|
|
77
|
-
for (const model of
|
|
66
|
+
for (const model of REQUIRED_CMS_MODELS) {
|
|
78
67
|
if (schemaModels.has(model)) {
|
|
79
68
|
console.log(` ${chalk.green("✓")} ${model}`);
|
|
80
69
|
present++;
|