@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
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
vi.mock('@actuate-media/cms-core', () => ({
|
|
4
4
|
extractPlainText: vi.fn((value: string) => value.replace(/<[^>]+>/g, ' ')),
|
|
5
5
|
hashContent: vi.fn(async () => 'hash-1'),
|
|
6
6
|
sanitizeHtml: vi.fn((value: string) => value.replace(/<script[\s\S]*?<\/script>/gi, '')),
|
|
7
7
|
createInitialAdmin: vi.fn(),
|
|
8
|
-
}))
|
|
8
|
+
}))
|
|
9
9
|
|
|
10
|
-
import { createSeedDocument, ensureSeedAdmin, normalizeSeedPayload } from '../commands/seed.js'
|
|
10
|
+
import { createSeedDocument, ensureSeedAdmin, normalizeSeedPayload } from '../commands/seed.js'
|
|
11
11
|
|
|
12
12
|
afterEach(() => {
|
|
13
|
-
vi.unstubAllEnvs()
|
|
14
|
-
})
|
|
13
|
+
vi.unstubAllEnvs()
|
|
14
|
+
})
|
|
15
15
|
|
|
16
16
|
describe('seed payload normalization', () => {
|
|
17
17
|
it('separates globals from collection documents', () => {
|
|
@@ -30,38 +30,38 @@ describe('seed payload normalization', () => {
|
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
32
|
},
|
|
33
|
-
})
|
|
33
|
+
})
|
|
34
34
|
|
|
35
35
|
expect(normalized.globals).toEqual([
|
|
36
36
|
{
|
|
37
37
|
slug: 'site-settings',
|
|
38
38
|
data: { siteName: 'MaidPro', phone: '555-0100' },
|
|
39
39
|
},
|
|
40
|
-
])
|
|
40
|
+
])
|
|
41
41
|
expect(normalized.documents).toEqual([
|
|
42
42
|
{
|
|
43
43
|
collection: 'pages',
|
|
44
44
|
data: { title: 'Home', slug: 'home' },
|
|
45
45
|
status: 'PUBLISHED',
|
|
46
46
|
},
|
|
47
|
-
])
|
|
48
|
-
})
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
49
|
|
|
50
50
|
it('keeps backwards-compatible collection-key seed files', () => {
|
|
51
51
|
const normalized = normalizeSeedPayload({
|
|
52
52
|
pages: [{ title: 'Home', slug: 'home' }],
|
|
53
|
-
})
|
|
53
|
+
})
|
|
54
54
|
|
|
55
|
-
expect(normalized.globals).toEqual([])
|
|
55
|
+
expect(normalized.globals).toEqual([])
|
|
56
56
|
expect(normalized.documents).toEqual([
|
|
57
57
|
{
|
|
58
58
|
collection: 'pages',
|
|
59
59
|
data: { title: 'Home', slug: 'home' },
|
|
60
60
|
status: 'DRAFT',
|
|
61
61
|
},
|
|
62
|
-
])
|
|
63
|
-
})
|
|
64
|
-
})
|
|
62
|
+
])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
65
|
|
|
66
66
|
describe('seed admin handling', () => {
|
|
67
67
|
it('refuses to create a first admin without credentials', async () => {
|
|
@@ -69,11 +69,11 @@ describe('seed admin handling', () => {
|
|
|
69
69
|
user: {
|
|
70
70
|
findFirst: vi.fn().mockResolvedValue(null),
|
|
71
71
|
},
|
|
72
|
-
}
|
|
72
|
+
}
|
|
73
73
|
|
|
74
|
-
await expect(ensureSeedAdmin(db)).rejects.toThrow('CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD')
|
|
75
|
-
})
|
|
76
|
-
})
|
|
74
|
+
await expect(ensureSeedAdmin(db)).rejects.toThrow('CMS_ADMIN_EMAIL and CMS_ADMIN_PASSWORD')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
77
|
|
|
78
78
|
describe('seed document creation', () => {
|
|
79
79
|
it('creates documents with versions, content hashes, and sanitized HTML', async () => {
|
|
@@ -84,10 +84,10 @@ describe('seed document creation', () => {
|
|
|
84
84
|
version: {
|
|
85
85
|
create: vi.fn(),
|
|
86
86
|
},
|
|
87
|
-
}
|
|
87
|
+
}
|
|
88
88
|
const db = {
|
|
89
89
|
$transaction: vi.fn(async (fn) => fn(tx)),
|
|
90
|
-
}
|
|
90
|
+
}
|
|
91
91
|
|
|
92
92
|
await createSeedDocument(db, 'admin-1', {
|
|
93
93
|
collection: 'pages',
|
|
@@ -97,7 +97,7 @@ describe('seed document creation', () => {
|
|
|
97
97
|
slug: 'home',
|
|
98
98
|
content: '<p>Hello</p><script>alert(1)</script>',
|
|
99
99
|
},
|
|
100
|
-
})
|
|
100
|
+
})
|
|
101
101
|
|
|
102
102
|
expect(tx.document.create).toHaveBeenCalledWith({
|
|
103
103
|
data: expect.objectContaining({
|
|
@@ -114,13 +114,13 @@ describe('seed document creation', () => {
|
|
|
114
114
|
content: '<p>Hello</p>',
|
|
115
115
|
}),
|
|
116
116
|
}),
|
|
117
|
-
})
|
|
117
|
+
})
|
|
118
118
|
expect(tx.version.create).toHaveBeenCalledWith({
|
|
119
119
|
data: expect.objectContaining({
|
|
120
120
|
documentId: 'doc-1',
|
|
121
121
|
changedById: 'admin-1',
|
|
122
122
|
changeType: 'CREATE',
|
|
123
123
|
}),
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
})
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
})
|
package/src/commands/db-init.ts
CHANGED
|
@@ -1,384 +1,211 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { readFile, writeFile, access } from
|
|
3
|
-
import { resolve, join } from
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { readFile, writeFile, access } from 'node:fs/promises'
|
|
3
|
+
import { resolve, join, dirname } from 'node:path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { execSync, type ExecSyncOptions } from 'node:child_process'
|
|
6
|
+
import ora from 'ora'
|
|
7
|
+
import { logger } from '../utils/logger.js'
|
|
7
8
|
|
|
8
|
-
const CMS_SCHEMA_MARKER =
|
|
9
|
+
const CMS_SCHEMA_MARKER = '// ── Actuate CMS models'
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
9
12
|
|
|
10
13
|
export let runDbInitCommand = (command: string, options: ExecSyncOptions): void => {
|
|
11
|
-
execSync(command, options)
|
|
12
|
-
}
|
|
14
|
+
execSync(command, options)
|
|
15
|
+
}
|
|
13
16
|
|
|
14
|
-
const defaultDbInitCommandRunner = runDbInitCommand
|
|
17
|
+
const defaultDbInitCommandRunner = runDbInitCommand
|
|
15
18
|
|
|
16
19
|
export function setDbInitCommandRunner(runner: typeof runDbInitCommand): void {
|
|
17
|
-
runDbInitCommand = runner
|
|
20
|
+
runDbInitCommand = runner
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export function resetDbInitCommandRunner(): void {
|
|
21
|
-
runDbInitCommand = defaultDbInitCommandRunner
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
runDbInitCommand = defaultDbInitCommandRunner
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads the canonical Prisma schema shipped by the installed
|
|
29
|
+
* `@actuate-media/cms-core` package. This is the single source of truth — it has
|
|
30
|
+
* the correct `@@map("actuate_*")` table names and every model the API handlers
|
|
31
|
+
* expect. Previously `db:init` injected a hand-maintained copy that drifted
|
|
32
|
+
* (missing `@@map`, missing models), producing a database the runtime couldn't
|
|
33
|
+
* query. Overridable for tests.
|
|
34
|
+
*/
|
|
35
|
+
export let readCanonicalCmsSchema = async (): Promise<string | null> => {
|
|
36
|
+
const candidates: string[] = []
|
|
25
37
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
// `./prisma/schema` is an exported subpath, so this resolves inside the
|
|
39
|
+
// package's `prisma/` directory without depending on the build output.
|
|
40
|
+
const exported = require.resolve('@actuate-media/cms-core/prisma/schema')
|
|
41
|
+
candidates.push(join(dirname(exported), 'schema.prisma'))
|
|
28
42
|
} catch {
|
|
29
|
-
|
|
43
|
+
/* exports map missing the subpath — fall through to the main entry */
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const mainEntry = require.resolve('@actuate-media/cms-core')
|
|
47
|
+
candidates.push(join(dirname(mainEntry), '..', 'prisma', 'schema.prisma'))
|
|
48
|
+
} catch {
|
|
49
|
+
/* package not installed */
|
|
30
50
|
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getCmsSchemaFragment(): string {
|
|
34
|
-
return `
|
|
35
|
-
// ── Actuate CMS models ─────────────────────────────────────────────────────
|
|
36
|
-
// Auto-injected by \`actuate db:init\`. Do not remove this marker comment.
|
|
37
|
-
// Schema version: 1
|
|
38
|
-
|
|
39
|
-
enum DocumentStatus {
|
|
40
|
-
DRAFT
|
|
41
|
-
PUBLISHED
|
|
42
|
-
ARCHIVED
|
|
43
|
-
SCHEDULED
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
model User {
|
|
47
|
-
id String @id @default(cuid())
|
|
48
|
-
email String @unique
|
|
49
|
-
name String @default("")
|
|
50
|
-
role String @default("EDITOR")
|
|
51
|
-
passwordHash String?
|
|
52
|
-
isActive Boolean @default(true)
|
|
53
|
-
isApproved Boolean @default(false)
|
|
54
|
-
emailVerified Boolean @default(false)
|
|
55
|
-
totpEnabled Boolean @default(false)
|
|
56
|
-
totpSecret String?
|
|
57
|
-
backupCodes Json?
|
|
58
|
-
oauthProvider String?
|
|
59
|
-
oauthId String?
|
|
60
|
-
createdAt DateTime @default(now())
|
|
61
|
-
updatedAt DateTime @updatedAt
|
|
62
|
-
|
|
63
|
-
sessions Session[]
|
|
64
|
-
documentsCreated Document[] @relation("DocumentCreatedBy")
|
|
65
|
-
documentsUpdated Document[] @relation("DocumentUpdatedBy")
|
|
66
|
-
documentsReviewed Document[] @relation("DocumentReviewer")
|
|
67
|
-
versions Version[]
|
|
68
|
-
mediaUploaded Media[] @relation("MediaUploadedBy")
|
|
69
|
-
auditLogs AuditLog[]
|
|
70
|
-
|
|
71
|
-
@@index([role])
|
|
72
|
-
@@index([isActive])
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
model Session {
|
|
76
|
-
id String @id @default(cuid())
|
|
77
|
-
userId String
|
|
78
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
79
|
-
token String? @unique
|
|
80
|
-
expiresAt DateTime
|
|
81
|
-
revokedAt DateTime?
|
|
82
|
-
ipAddress String?
|
|
83
|
-
userAgent String?
|
|
84
|
-
createdAt DateTime @default(now())
|
|
85
|
-
|
|
86
|
-
@@index([userId])
|
|
87
|
-
@@index([expiresAt])
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
model Document {
|
|
91
|
-
id String @id @default(cuid())
|
|
92
|
-
collection String
|
|
93
|
-
slug String?
|
|
94
|
-
title String?
|
|
95
|
-
data Json
|
|
96
|
-
status DocumentStatus @default(DRAFT)
|
|
97
|
-
plainText String? @db.Text
|
|
98
|
-
locale String?
|
|
99
|
-
folderId String?
|
|
100
|
-
structuredData Json?
|
|
101
|
-
workflowStage String?
|
|
102
|
-
reviewerId String?
|
|
103
|
-
reviewNote String? @db.Text
|
|
104
|
-
publishedAt DateTime?
|
|
105
|
-
scheduledAt DateTime?
|
|
106
|
-
scheduledUnpublishAt DateTime?
|
|
107
|
-
deletedAt DateTime?
|
|
108
|
-
contentHash String?
|
|
109
|
-
siteId String?
|
|
110
|
-
templateId String?
|
|
111
|
-
createdById String
|
|
112
|
-
updatedById String
|
|
113
|
-
createdAt DateTime @default(now())
|
|
114
|
-
updatedAt DateTime @updatedAt
|
|
115
|
-
|
|
116
|
-
createdBy User @relation("DocumentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict)
|
|
117
|
-
updatedBy User @relation("DocumentUpdatedBy", fields: [updatedById], references: [id], onDelete: Restrict)
|
|
118
|
-
reviewer User? @relation("DocumentReviewer", fields: [reviewerId], references: [id], onDelete: SetNull)
|
|
119
|
-
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
120
|
-
versions Version[]
|
|
121
|
-
formSubmissions FormSubmission[]
|
|
122
|
-
|
|
123
|
-
@@unique([collection, slug], name: "collection_slug")
|
|
124
|
-
@@index([collection])
|
|
125
|
-
@@index([status])
|
|
126
|
-
@@index([deletedAt])
|
|
127
|
-
@@index([publishedAt])
|
|
128
|
-
@@index([folderId])
|
|
129
|
-
@@index([locale])
|
|
130
|
-
@@index([scheduledAt])
|
|
131
|
-
@@index([scheduledUnpublishAt])
|
|
132
|
-
@@index([createdById])
|
|
133
|
-
@@index([updatedById])
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
model Media {
|
|
137
|
-
id String @id @default(cuid())
|
|
138
|
-
filename String
|
|
139
|
-
storageKey String @unique
|
|
140
|
-
mimeType String
|
|
141
|
-
fileSize Int
|
|
142
|
-
width Int?
|
|
143
|
-
height Int?
|
|
144
|
-
altText String?
|
|
145
|
-
title String?
|
|
146
|
-
blurHash String?
|
|
147
|
-
focalPointX Float?
|
|
148
|
-
focalPointY Float?
|
|
149
|
-
folderId String?
|
|
150
|
-
uploadedById String
|
|
151
|
-
createdAt DateTime @default(now())
|
|
152
|
-
updatedAt DateTime @updatedAt
|
|
153
|
-
|
|
154
|
-
uploadedBy User @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: Restrict)
|
|
155
|
-
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
156
|
-
|
|
157
|
-
@@index([folderId])
|
|
158
|
-
@@index([mimeType])
|
|
159
|
-
@@index([createdAt])
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
model Version {
|
|
163
|
-
id String @id @default(cuid())
|
|
164
|
-
documentId String
|
|
165
|
-
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
|
166
|
-
data Json
|
|
167
|
-
changedById String
|
|
168
|
-
changedBy User @relation(fields: [changedById], references: [id], onDelete: Restrict)
|
|
169
|
-
changeType String @default("UPDATE")
|
|
170
|
-
createdAt DateTime @default(now())
|
|
171
|
-
|
|
172
|
-
@@index([documentId])
|
|
173
|
-
@@index([createdAt])
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
model Folder {
|
|
177
|
-
id String @id @default(cuid())
|
|
178
|
-
name String
|
|
179
|
-
scope String
|
|
180
|
-
parentId String?
|
|
181
|
-
position Int @default(0)
|
|
182
|
-
createdAt DateTime @default(now())
|
|
183
|
-
|
|
184
|
-
parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
|
|
185
|
-
children Folder[] @relation("FolderTree")
|
|
186
|
-
documents Document[]
|
|
187
|
-
media Media[]
|
|
188
|
-
|
|
189
|
-
@@index([scope])
|
|
190
|
-
@@index([parentId])
|
|
191
|
-
@@index([scope, parentId, position])
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
model Redirect {
|
|
195
|
-
id String @id @default(cuid())
|
|
196
|
-
source String
|
|
197
|
-
destination String
|
|
198
|
-
statusCode Int @default(301)
|
|
199
|
-
isRegex Boolean @default(false)
|
|
200
|
-
notes String?
|
|
201
|
-
createdAt DateTime @default(now())
|
|
202
|
-
updatedAt DateTime @updatedAt
|
|
203
51
|
|
|
204
|
-
|
|
205
|
-
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
try {
|
|
54
|
+
return await readFile(candidate, 'utf-8')
|
|
55
|
+
} catch {
|
|
56
|
+
/* try the next candidate */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
206
60
|
}
|
|
207
61
|
|
|
208
|
-
|
|
209
|
-
id String @id @default(cuid())
|
|
210
|
-
formId String
|
|
211
|
-
form Document @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
212
|
-
data Json
|
|
213
|
-
attribution Json?
|
|
214
|
-
submittedAt DateTime @default(now())
|
|
215
|
-
createdAt DateTime @default(now())
|
|
62
|
+
const defaultSchemaReader = readCanonicalCmsSchema
|
|
216
63
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
@@index([createdAt])
|
|
64
|
+
export function setCanonicalSchemaReader(reader: typeof readCanonicalCmsSchema): void {
|
|
65
|
+
readCanonicalCmsSchema = reader
|
|
220
66
|
}
|
|
221
67
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
event String
|
|
225
|
-
userId String?
|
|
226
|
-
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
227
|
-
details Json?
|
|
228
|
-
ipAddress String?
|
|
229
|
-
userAgent String?
|
|
230
|
-
createdAt DateTime @default(now())
|
|
231
|
-
|
|
232
|
-
@@index([event])
|
|
233
|
-
@@index([userId])
|
|
234
|
-
@@index([createdAt])
|
|
68
|
+
export function resetCanonicalSchemaReader(): void {
|
|
69
|
+
readCanonicalCmsSchema = defaultSchemaReader
|
|
235
70
|
}
|
|
236
71
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
userId String
|
|
240
|
-
tokenHash String
|
|
241
|
-
expiresAt DateTime
|
|
242
|
-
usedAt DateTime?
|
|
243
|
-
createdAt DateTime @default(now())
|
|
244
|
-
|
|
245
|
-
@@index([tokenHash])
|
|
246
|
-
@@index([userId])
|
|
72
|
+
function netBraces(line: string): number {
|
|
73
|
+
return (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0)
|
|
247
74
|
}
|
|
248
75
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Strips the top-level `generator` and `datasource` blocks from a full Prisma
|
|
78
|
+
* schema, leaving only the `enum` and `model` definitions. The consumer keeps
|
|
79
|
+
* their own datasource/generator; we only contribute models.
|
|
80
|
+
*/
|
|
81
|
+
export function extractModelsFragment(fullSchema: string): string {
|
|
82
|
+
const lines = fullSchema.split(/\r?\n/)
|
|
83
|
+
const kept: string[] = []
|
|
84
|
+
let skipping = false
|
|
85
|
+
let depth = 0
|
|
255
86
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
createdAt DateTime @default(now())
|
|
271
|
-
updatedAt DateTime @updatedAt
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!skipping) {
|
|
89
|
+
if (/^\s*(generator|datasource)\s+[A-Za-z0-9_]+\s*\{/.test(line)) {
|
|
90
|
+
skipping = true
|
|
91
|
+
depth = netBraces(line)
|
|
92
|
+
if (depth <= 0) skipping = false
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
kept.push(line)
|
|
96
|
+
} else {
|
|
97
|
+
depth += netBraces(line)
|
|
98
|
+
if (depth <= 0) skipping = false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
272
101
|
|
|
273
|
-
|
|
274
|
-
@@index([placement])
|
|
102
|
+
return kept.join('\n').trim()
|
|
275
103
|
}
|
|
276
104
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
thumbnail String?
|
|
284
|
-
builtIn Boolean @default(false)
|
|
285
|
-
createdAt DateTime @default(now())
|
|
286
|
-
updatedAt DateTime @updatedAt
|
|
105
|
+
function wrapFragment(models: string): string {
|
|
106
|
+
return `
|
|
107
|
+
${CMS_SCHEMA_MARKER} ─────────────────────────────────────────────────────────
|
|
108
|
+
// Auto-injected by \`actuate db:init\` from the installed @actuate-media/cms-core
|
|
109
|
+
// package's canonical prisma/schema.prisma (the single source of truth). Do not
|
|
110
|
+
// edit by hand — re-run \`actuate db:init --force\` after upgrading cms-core.
|
|
287
111
|
|
|
288
|
-
|
|
289
|
-
|
|
112
|
+
${models}
|
|
113
|
+
`
|
|
290
114
|
}
|
|
291
115
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
usageCount Int @default(0)
|
|
300
|
-
createdAt DateTime @default(now())
|
|
301
|
-
updatedAt DateTime @updatedAt
|
|
302
|
-
|
|
303
|
-
@@index([category])
|
|
304
|
-
}
|
|
305
|
-
`;
|
|
116
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
await access(filePath)
|
|
119
|
+
return true
|
|
120
|
+
} catch {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
306
123
|
}
|
|
307
124
|
|
|
308
125
|
export function registerDbInitCommand(program: Command): void {
|
|
309
126
|
program
|
|
310
|
-
.command(
|
|
311
|
-
.description(
|
|
312
|
-
.option(
|
|
313
|
-
.option(
|
|
314
|
-
.option(
|
|
127
|
+
.command('db:init')
|
|
128
|
+
.description('Add Actuate CMS models (from the installed cms-core) to your Prisma schema')
|
|
129
|
+
.option('--schema <path>', 'Path to schema.prisma', 'prisma/schema.prisma')
|
|
130
|
+
.option('--migrate', 'Run prisma migrate dev after adding models')
|
|
131
|
+
.option('--force', 'Overwrite existing CMS models if present')
|
|
315
132
|
.action(async (opts: { schema: string; migrate?: boolean; force?: boolean }) => {
|
|
316
|
-
const schemaPath = resolve(process.cwd(), opts.schema)
|
|
133
|
+
const schemaPath = resolve(process.cwd(), opts.schema)
|
|
317
134
|
|
|
318
135
|
if (!(await fileExists(schemaPath))) {
|
|
319
|
-
logger.error(`Schema file not found at ${schemaPath}`)
|
|
320
|
-
logger.info(
|
|
321
|
-
process.exitCode = 1
|
|
322
|
-
return
|
|
136
|
+
logger.error(`Schema file not found at ${schemaPath}`)
|
|
137
|
+
logger.info('Run `npx prisma init` first, or specify --schema <path>.')
|
|
138
|
+
process.exitCode = 1
|
|
139
|
+
return
|
|
323
140
|
}
|
|
324
141
|
|
|
325
|
-
const spinner = ora(
|
|
142
|
+
const spinner = ora('Reading canonical Actuate schema...').start()
|
|
143
|
+
|
|
144
|
+
const canonical = await readCanonicalCmsSchema()
|
|
145
|
+
if (!canonical) {
|
|
146
|
+
spinner.fail('Could not locate @actuate-media/cms-core.')
|
|
147
|
+
logger.info('Install it first: `npm install @actuate-media/cms-core`.')
|
|
148
|
+
process.exitCode = 1
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const fragment = wrapFragment(extractModelsFragment(canonical))
|
|
326
152
|
|
|
327
|
-
|
|
153
|
+
spinner.text = 'Reading Prisma schema...'
|
|
154
|
+
let content: string
|
|
328
155
|
try {
|
|
329
|
-
content = await readFile(schemaPath,
|
|
156
|
+
content = await readFile(schemaPath, 'utf-8')
|
|
330
157
|
} catch (err) {
|
|
331
|
-
spinner.fail(
|
|
332
|
-
logger.error(err instanceof Error ? err.message : String(err))
|
|
333
|
-
process.exitCode = 1
|
|
334
|
-
return
|
|
158
|
+
spinner.fail('Failed to read schema file.')
|
|
159
|
+
logger.error(err instanceof Error ? err.message : String(err))
|
|
160
|
+
process.exitCode = 1
|
|
161
|
+
return
|
|
335
162
|
}
|
|
336
163
|
|
|
337
164
|
if (content.includes(CMS_SCHEMA_MARKER)) {
|
|
338
165
|
if (!opts.force) {
|
|
339
|
-
spinner.info(
|
|
340
|
-
return
|
|
166
|
+
spinner.info('Actuate CMS models already present in schema. Use --force to overwrite.')
|
|
167
|
+
return
|
|
341
168
|
}
|
|
342
|
-
spinner.text =
|
|
343
|
-
const markerIndex = content.indexOf(CMS_SCHEMA_MARKER)
|
|
344
|
-
content = content.substring(0, markerIndex).trimEnd() +
|
|
169
|
+
spinner.text = 'Removing existing CMS models...'
|
|
170
|
+
const markerIndex = content.indexOf(CMS_SCHEMA_MARKER)
|
|
171
|
+
content = content.substring(0, markerIndex).trimEnd() + '\n'
|
|
345
172
|
}
|
|
346
173
|
|
|
347
|
-
spinner.text =
|
|
348
|
-
const updatedContent = content.trimEnd() +
|
|
174
|
+
spinner.text = 'Adding Actuate CMS models...'
|
|
175
|
+
const updatedContent = content.trimEnd() + '\n' + fragment
|
|
349
176
|
|
|
350
177
|
try {
|
|
351
|
-
await writeFile(schemaPath, updatedContent)
|
|
352
|
-
spinner.succeed(
|
|
178
|
+
await writeFile(schemaPath, updatedContent)
|
|
179
|
+
spinner.succeed('Actuate CMS models added to schema.')
|
|
353
180
|
} catch (err) {
|
|
354
|
-
spinner.fail(
|
|
355
|
-
logger.error(err instanceof Error ? err.message : String(err))
|
|
356
|
-
process.exitCode = 1
|
|
357
|
-
return
|
|
181
|
+
spinner.fail('Failed to write schema file.')
|
|
182
|
+
logger.error(err instanceof Error ? err.message : String(err))
|
|
183
|
+
process.exitCode = 1
|
|
184
|
+
return
|
|
358
185
|
}
|
|
359
186
|
|
|
360
|
-
const execOpts: ExecSyncOptions = { stdio:
|
|
187
|
+
const execOpts: ExecSyncOptions = { stdio: 'inherit', cwd: process.cwd() }
|
|
361
188
|
|
|
362
|
-
const genSpinner = ora(
|
|
189
|
+
const genSpinner = ora('Running prisma generate...').start()
|
|
363
190
|
try {
|
|
364
|
-
genSpinner.stop()
|
|
365
|
-
runDbInitCommand(
|
|
366
|
-
logger.success(
|
|
191
|
+
genSpinner.stop()
|
|
192
|
+
runDbInitCommand('npx prisma generate', execOpts)
|
|
193
|
+
logger.success('Prisma client generated.')
|
|
367
194
|
} catch {
|
|
368
|
-
logger.warn(
|
|
195
|
+
logger.warn('prisma generate failed. You may need to set DATABASE_URL first.')
|
|
369
196
|
}
|
|
370
197
|
|
|
371
198
|
if (opts.migrate) {
|
|
372
|
-
const migSpinner = ora(
|
|
199
|
+
const migSpinner = ora('Running prisma migrate dev...').start()
|
|
373
200
|
try {
|
|
374
|
-
migSpinner.stop()
|
|
375
|
-
runDbInitCommand(
|
|
376
|
-
logger.success(
|
|
201
|
+
migSpinner.stop()
|
|
202
|
+
runDbInitCommand('npx prisma migrate dev --name actuate-cms-init', execOpts)
|
|
203
|
+
logger.success('Migration created and applied.')
|
|
377
204
|
} catch {
|
|
378
|
-
logger.warn(
|
|
205
|
+
logger.warn('prisma migrate dev failed. Run it manually after setting DATABASE_URL.')
|
|
379
206
|
}
|
|
380
207
|
} else {
|
|
381
|
-
logger.info(
|
|
208
|
+
logger.info('Run `npx prisma migrate dev --name actuate-cms` to create the migration.')
|
|
382
209
|
}
|
|
383
|
-
})
|
|
210
|
+
})
|
|
384
211
|
}
|