@actuate-media/cms-core 0.16.0 → 0.17.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/dist/__tests__/api/collections-ai-create.test.d.ts +2 -0
- package/dist/__tests__/api/collections-ai-create.test.d.ts.map +1 -0
- package/dist/__tests__/api/collections-ai-create.test.js +313 -0
- package/dist/__tests__/api/collections-ai-create.test.js.map +1 -0
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +142 -0
- package/dist/api/handlers.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections-ai-create.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/collections-ai-create.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
// IMPORTANT: vi.mock calls are hoisted. We mock `@actuate-media/plugin-ai`
|
|
3
|
+
// so the dynamic import inside handlers.ts resolves to a deterministic
|
|
4
|
+
// generator instead of trying to hit a real LLM provider.
|
|
5
|
+
vi.mock('@actuate-media/plugin-ai', () => ({
|
|
6
|
+
generateDocumentContent: vi.fn(async (input) => ({
|
|
7
|
+
data: {
|
|
8
|
+
title: 'Five Spring Cleaning Tips',
|
|
9
|
+
slug: 'five-spring-cleaning-tips',
|
|
10
|
+
excerpt: 'A practical guide.',
|
|
11
|
+
body: 'When spring arrives, tackle these five tasks first.',
|
|
12
|
+
},
|
|
13
|
+
title: 'Five Spring Cleaning Tips',
|
|
14
|
+
slug: 'five-spring-cleaning-tips',
|
|
15
|
+
seo: {
|
|
16
|
+
metaTitle: 'Spring Cleaning Tips',
|
|
17
|
+
metaDescription: 'Five practical spring cleaning tips for busy households.',
|
|
18
|
+
schemaType: input?.collection?.seo?.defaultSchemaType ?? 'Article',
|
|
19
|
+
},
|
|
20
|
+
usage: { promptTokens: 100, completionTokens: 250 },
|
|
21
|
+
durationMs: 42,
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
25
|
+
import { initDB } from '../../db.js';
|
|
26
|
+
import { generateApiKey } from '../../security/api-key-enhanced.js';
|
|
27
|
+
const VALID_SECRET = 'a'.repeat(48);
|
|
28
|
+
function createMockDB(opts) {
|
|
29
|
+
const apiKey = opts.apiKey;
|
|
30
|
+
let lastCreatedDoc = null;
|
|
31
|
+
const dbMock = {
|
|
32
|
+
apiKey: {
|
|
33
|
+
findMany: async () => (apiKey ? [apiKey] : []),
|
|
34
|
+
findUnique: async ({ where }) => {
|
|
35
|
+
if (!apiKey)
|
|
36
|
+
return null;
|
|
37
|
+
if (apiKey.keyHash !== where.keyHash)
|
|
38
|
+
return null;
|
|
39
|
+
return apiKey;
|
|
40
|
+
},
|
|
41
|
+
update: async () => apiKey,
|
|
42
|
+
},
|
|
43
|
+
document: {
|
|
44
|
+
findMany: async () => [],
|
|
45
|
+
findUnique: async () => lastCreatedDoc,
|
|
46
|
+
findFirst: async ({ where }) => {
|
|
47
|
+
// Return the most recently created doc if its id matches; otherwise
|
|
48
|
+
// null. updateDocument calls findFirst({ id, collection, deletedAt: null }).
|
|
49
|
+
if (!lastCreatedDoc)
|
|
50
|
+
return null;
|
|
51
|
+
if (where?.id && where.id !== lastCreatedDoc.id)
|
|
52
|
+
return null;
|
|
53
|
+
return lastCreatedDoc;
|
|
54
|
+
},
|
|
55
|
+
count: async () => 0,
|
|
56
|
+
create: async ({ data }) => {
|
|
57
|
+
lastCreatedDoc = {
|
|
58
|
+
...data,
|
|
59
|
+
id: 'doc-ai-1',
|
|
60
|
+
createdAt: new Date(),
|
|
61
|
+
updatedAt: new Date(),
|
|
62
|
+
};
|
|
63
|
+
return lastCreatedDoc;
|
|
64
|
+
},
|
|
65
|
+
update: async ({ data }) => {
|
|
66
|
+
lastCreatedDoc = { ...lastCreatedDoc, ...data, updatedAt: new Date() };
|
|
67
|
+
return lastCreatedDoc;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
version: {
|
|
71
|
+
create: async ({ data }) => ({ ...data, id: 'ver-1' }),
|
|
72
|
+
findMany: async () => [],
|
|
73
|
+
},
|
|
74
|
+
documentRevision: {
|
|
75
|
+
create: async ({ data }) => ({ ...data, id: 'rev-1' }),
|
|
76
|
+
},
|
|
77
|
+
auditLog: { create: async () => ({}) },
|
|
78
|
+
webhook: { findMany: async () => [] },
|
|
79
|
+
webhookDelivery: { create: async () => ({}) },
|
|
80
|
+
user: {},
|
|
81
|
+
session: {},
|
|
82
|
+
media: {},
|
|
83
|
+
};
|
|
84
|
+
// createDocument may wrap the create in a transaction. Run the callback
|
|
85
|
+
// against the mock itself so the same model proxies fire.
|
|
86
|
+
dbMock.$transaction = async (arg) => {
|
|
87
|
+
if (typeof arg === 'function') {
|
|
88
|
+
return await arg(dbMock);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(arg)) {
|
|
91
|
+
return Promise.all(arg);
|
|
92
|
+
}
|
|
93
|
+
return arg;
|
|
94
|
+
};
|
|
95
|
+
return dbMock;
|
|
96
|
+
}
|
|
97
|
+
async function makeKey(scopes) {
|
|
98
|
+
const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
|
|
99
|
+
return {
|
|
100
|
+
key,
|
|
101
|
+
record: {
|
|
102
|
+
id: 'apikey-ai',
|
|
103
|
+
keyHash,
|
|
104
|
+
keyPrefix,
|
|
105
|
+
userId: 'user-1',
|
|
106
|
+
scopes,
|
|
107
|
+
ipRestrictions: null,
|
|
108
|
+
expiresAt: null,
|
|
109
|
+
lastUsedAt: null,
|
|
110
|
+
revokedAt: null,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const postsCollection = {
|
|
115
|
+
slug: 'posts',
|
|
116
|
+
labels: { singular: 'Post', plural: 'Posts' },
|
|
117
|
+
type: 'post',
|
|
118
|
+
seo: { defaultSchemaType: 'BlogPosting' },
|
|
119
|
+
fields: {
|
|
120
|
+
title: { type: 'text', label: 'Title', required: true },
|
|
121
|
+
slug: { type: 'slug', label: 'Slug', required: true },
|
|
122
|
+
excerpt: { type: 'text', label: 'Excerpt' },
|
|
123
|
+
body: { type: 'richText', label: 'Body' },
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const pagesCollection = {
|
|
127
|
+
slug: 'pages',
|
|
128
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
129
|
+
type: 'page',
|
|
130
|
+
fields: {
|
|
131
|
+
title: { type: 'text', label: 'Title', required: true },
|
|
132
|
+
slug: { type: 'slug', label: 'Slug', required: true },
|
|
133
|
+
layout: { type: 'blocks', label: 'Layout' },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
describe('POST /collections/:slug/ai-create', () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
delete globalThis.__actuateConfig;
|
|
139
|
+
process.env.CMS_SECRET = VALID_SECRET;
|
|
140
|
+
});
|
|
141
|
+
it('creates a document via AI when caller has create scope on the collection', async () => {
|
|
142
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
|
|
143
|
+
const db = createMockDB({ apiKey: record });
|
|
144
|
+
initDB(db);
|
|
145
|
+
const handler = handleActuateAPI({
|
|
146
|
+
prismaClient: db,
|
|
147
|
+
config: { collections: { posts: postsCollection } },
|
|
148
|
+
});
|
|
149
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${key}`,
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ prompt: 'Write a post about spring cleaning' }),
|
|
156
|
+
}));
|
|
157
|
+
const body = await response.json();
|
|
158
|
+
expect({ status: response.status, body }).toMatchObject({ status: 201 });
|
|
159
|
+
expect(body.data.document.id).toBe('doc-ai-1');
|
|
160
|
+
expect(body.data.document.title).toBe('Five Spring Cleaning Tips');
|
|
161
|
+
expect(body.data.document.slug).toBe('five-spring-cleaning-tips');
|
|
162
|
+
expect(body.data.document.status).toBe('DRAFT');
|
|
163
|
+
// SEO metadata flows back through the generation block (not pageSettings,
|
|
164
|
+
// which is a page-builder-only wire shape).
|
|
165
|
+
expect(body.data.generation.seo.schemaType).toBe('BlogPosting');
|
|
166
|
+
expect(body.data.generation.usage.completionTokens).toBe(250);
|
|
167
|
+
});
|
|
168
|
+
it('returns 400 when prompt is missing', async () => {
|
|
169
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
|
|
170
|
+
const db = createMockDB({ apiKey: record });
|
|
171
|
+
initDB(db);
|
|
172
|
+
const handler = handleActuateAPI({
|
|
173
|
+
prismaClient: db,
|
|
174
|
+
config: { collections: { posts: postsCollection } },
|
|
175
|
+
});
|
|
176
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
179
|
+
body: JSON.stringify({}),
|
|
180
|
+
}));
|
|
181
|
+
expect(response.status).toBe(400);
|
|
182
|
+
const body = await response.json();
|
|
183
|
+
expect(body.error).toContain('prompt');
|
|
184
|
+
});
|
|
185
|
+
it('returns 404 when the collection is not configured', async () => {
|
|
186
|
+
const { key, record } = await makeKey({ admin: true });
|
|
187
|
+
const db = createMockDB({ apiKey: record });
|
|
188
|
+
initDB(db);
|
|
189
|
+
const handler = handleActuateAPI({
|
|
190
|
+
prismaClient: db,
|
|
191
|
+
config: { collections: { posts: postsCollection } },
|
|
192
|
+
});
|
|
193
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/no-such-collection/ai-create', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ prompt: 'test' }),
|
|
197
|
+
}));
|
|
198
|
+
expect(response.status).toBe(404);
|
|
199
|
+
});
|
|
200
|
+
it('refuses page-builder collections and points to /page-builder/create', async () => {
|
|
201
|
+
const { key, record } = await makeKey({ collections: ['pages'], actions: ['create'] });
|
|
202
|
+
const db = createMockDB({ apiKey: record });
|
|
203
|
+
initDB(db);
|
|
204
|
+
const handler = handleActuateAPI({
|
|
205
|
+
prismaClient: db,
|
|
206
|
+
config: { collections: { pages: pagesCollection } },
|
|
207
|
+
});
|
|
208
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/pages/ai-create', {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify({ prompt: 'Create a homepage' }),
|
|
212
|
+
}));
|
|
213
|
+
expect(response.status).toBe(400);
|
|
214
|
+
const body = await response.json();
|
|
215
|
+
expect(body.error).toContain('page-builder/create');
|
|
216
|
+
});
|
|
217
|
+
it('rejects callers without create scope on the target collection', async () => {
|
|
218
|
+
// Key has read scope only.
|
|
219
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
|
|
220
|
+
const db = createMockDB({ apiKey: record });
|
|
221
|
+
initDB(db);
|
|
222
|
+
const handler = handleActuateAPI({
|
|
223
|
+
prismaClient: db,
|
|
224
|
+
config: { collections: { posts: postsCollection } },
|
|
225
|
+
});
|
|
226
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({ prompt: 'test' }),
|
|
230
|
+
}));
|
|
231
|
+
expect(response.status).toBe(403);
|
|
232
|
+
});
|
|
233
|
+
it('publishes immediately when publish: true is passed AND caller has admin scope', async () => {
|
|
234
|
+
// Publish requires admin scope on the API key — under the hood that
|
|
235
|
+
// maps to role=ADMIN, which clears the publish gate in updateDocument.
|
|
236
|
+
// A non-admin key that has only create+update will still create the
|
|
237
|
+
// draft but be denied the publish transition (see the next test).
|
|
238
|
+
const { key, record } = await makeKey({ admin: true });
|
|
239
|
+
const db = createMockDB({ apiKey: record });
|
|
240
|
+
initDB(db);
|
|
241
|
+
const handler = handleActuateAPI({
|
|
242
|
+
prismaClient: db,
|
|
243
|
+
config: { collections: { posts: postsCollection } },
|
|
244
|
+
});
|
|
245
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ prompt: 'test', publish: true }),
|
|
249
|
+
}));
|
|
250
|
+
expect(response.status).toBe(201);
|
|
251
|
+
const body = await response.json();
|
|
252
|
+
expect(body.data.document.status).toBe('PUBLISHED');
|
|
253
|
+
});
|
|
254
|
+
it('falls back to DRAFT + warning when publish requested but update scope is missing', async () => {
|
|
255
|
+
// create-only key — draft will be created, publish will fail.
|
|
256
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
|
|
257
|
+
const db = createMockDB({ apiKey: record });
|
|
258
|
+
initDB(db);
|
|
259
|
+
const handler = handleActuateAPI({
|
|
260
|
+
prismaClient: db,
|
|
261
|
+
config: { collections: { posts: postsCollection } },
|
|
262
|
+
});
|
|
263
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
266
|
+
body: JSON.stringify({ prompt: 'test', publish: true }),
|
|
267
|
+
}));
|
|
268
|
+
expect(response.status).toBe(201);
|
|
269
|
+
const body = await response.json();
|
|
270
|
+
expect(body.data.document.status).toBe('DRAFT');
|
|
271
|
+
expect(body.warning).toMatch(/publish failed/i);
|
|
272
|
+
});
|
|
273
|
+
it('honors caller-supplied title and slug overrides', async () => {
|
|
274
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
|
|
275
|
+
const db = createMockDB({ apiKey: record });
|
|
276
|
+
initDB(db);
|
|
277
|
+
const handler = handleActuateAPI({
|
|
278
|
+
prismaClient: db,
|
|
279
|
+
config: { collections: { posts: postsCollection } },
|
|
280
|
+
});
|
|
281
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
prompt: 'test',
|
|
286
|
+
title: 'My Custom Title',
|
|
287
|
+
slug: 'my-custom-slug',
|
|
288
|
+
}),
|
|
289
|
+
}));
|
|
290
|
+
expect(response.status).toBe(201);
|
|
291
|
+
const body = await response.json();
|
|
292
|
+
expect(body.data.document.title).toBe('My Custom Title');
|
|
293
|
+
expect(body.data.document.slug).toBe('my-custom-slug');
|
|
294
|
+
});
|
|
295
|
+
it('returns 400 when prompt exceeds 4000 char limit', async () => {
|
|
296
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
|
|
297
|
+
const db = createMockDB({ apiKey: record });
|
|
298
|
+
initDB(db);
|
|
299
|
+
const handler = handleActuateAPI({
|
|
300
|
+
prismaClient: db,
|
|
301
|
+
config: { collections: { posts: postsCollection } },
|
|
302
|
+
});
|
|
303
|
+
const response = await handler(new Request('https://example.com/api/cms/collections/posts/ai-create', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({ prompt: 'A'.repeat(5000) }),
|
|
307
|
+
}));
|
|
308
|
+
expect(response.status).toBe(400);
|
|
309
|
+
const body = await response.json();
|
|
310
|
+
expect(body.error).toContain('exceeds');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
//# sourceMappingURL=collections-ai-create.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections-ai-create.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/collections-ai-create.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAigB5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AA2ND,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAigB5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AA2ND,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAuzKzD"}
|
package/dist/api/handlers.js
CHANGED
|
@@ -5333,6 +5333,148 @@ export function registerCMSRoutes(router) {
|
|
|
5333
5333
|
return internalError(err, 'page-builder create');
|
|
5334
5334
|
}
|
|
5335
5335
|
});
|
|
5336
|
+
/**
|
|
5337
|
+
* Generic AI content authoring for ANY collection.
|
|
5338
|
+
*
|
|
5339
|
+
* `POST /collections/:slug/ai-create` is the non-page counterpart to
|
|
5340
|
+
* `/page-builder/create`. It takes a free-form prompt, looks up the
|
|
5341
|
+
* collection's field schema from the running config, asks the AI provider
|
|
5342
|
+
* to produce structured JSON matching that schema, validates / coerces the
|
|
5343
|
+
* output, and persists the result as a draft (or published) document.
|
|
5344
|
+
*
|
|
5345
|
+
* Used by:
|
|
5346
|
+
* - the admin "Generate from prompt" affordance on the collection list
|
|
5347
|
+
* - the MCP server's `create_in_collection` / `create_blog_post` / etc.
|
|
5348
|
+
* - AI agents calling the REST API directly
|
|
5349
|
+
*/
|
|
5350
|
+
router.post('/collections/:slug/ai-create', async (request, params) => {
|
|
5351
|
+
try {
|
|
5352
|
+
const auth = await requireAuth(request);
|
|
5353
|
+
if (auth.error)
|
|
5354
|
+
return auth.error;
|
|
5355
|
+
const targetCollection = params.slug;
|
|
5356
|
+
const scopeErr = requireCollectionScope(auth.session, targetCollection, 'create');
|
|
5357
|
+
if (scopeErr)
|
|
5358
|
+
return scopeErr;
|
|
5359
|
+
if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-gen:${auth.session.userId}`))) {
|
|
5360
|
+
return errorResponse('AI generation rate limit reached. Try again in an hour.', 429);
|
|
5361
|
+
}
|
|
5362
|
+
const body = (await request.json());
|
|
5363
|
+
if (!body.prompt || typeof body.prompt !== 'string') {
|
|
5364
|
+
return errorResponse('prompt is required', 400);
|
|
5365
|
+
}
|
|
5366
|
+
if (body.prompt.length > AI_PROMPT_MAX_CHARS) {
|
|
5367
|
+
return errorResponse(`prompt exceeds ${AI_PROMPT_MAX_CHARS} character limit`, 400);
|
|
5368
|
+
}
|
|
5369
|
+
if (body.context && body.context.length > AI_CONTEXT_MAX_CHARS) {
|
|
5370
|
+
return errorResponse(`context exceeds ${AI_CONTEXT_MAX_CHARS} character limit`, 400);
|
|
5371
|
+
}
|
|
5372
|
+
const cfg = getActuateConfig();
|
|
5373
|
+
const collection = cfg?.collections?.[targetCollection];
|
|
5374
|
+
if (!collection) {
|
|
5375
|
+
return errorResponse(`Collection "${targetCollection}" not found`, 404);
|
|
5376
|
+
}
|
|
5377
|
+
// Refuse page-builder-driven collections — those have a layout tree
|
|
5378
|
+
// (not flat field data) and should use `/page-builder/create` instead.
|
|
5379
|
+
const hasLayoutField = Object.values(collection.fields).some((f) => f.type === 'blocks');
|
|
5380
|
+
if (hasLayoutField) {
|
|
5381
|
+
return errorResponse(`Collection "${targetCollection}" uses the page builder. Use POST /api/cms/page-builder/create instead.`, 400);
|
|
5382
|
+
}
|
|
5383
|
+
let generateDocumentContent = null;
|
|
5384
|
+
try {
|
|
5385
|
+
const aiModule = await importAIPlugin();
|
|
5386
|
+
generateDocumentContent = aiModule.generateDocumentContent;
|
|
5387
|
+
}
|
|
5388
|
+
catch {
|
|
5389
|
+
return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use content authoring.', 501);
|
|
5390
|
+
}
|
|
5391
|
+
if (!generateDocumentContent) {
|
|
5392
|
+
return errorResponse('AI plugin missing `generateDocumentContent`. Upgrade @actuate-media/plugin-ai to >= 0.2.0.', 501);
|
|
5393
|
+
}
|
|
5394
|
+
const result = await generateDocumentContent({
|
|
5395
|
+
prompt: body.prompt,
|
|
5396
|
+
collection: {
|
|
5397
|
+
slug: collection.slug,
|
|
5398
|
+
labels: collection.labels,
|
|
5399
|
+
type: collection.type,
|
|
5400
|
+
fields: collection.fields,
|
|
5401
|
+
seo: collection.seo,
|
|
5402
|
+
},
|
|
5403
|
+
context: { existingContent: body.context, targetAudience: body.targetAudience },
|
|
5404
|
+
tone: body.tone,
|
|
5405
|
+
...(typeof body.maxTokens === 'number' ? { maxTokens: body.maxTokens } : {}),
|
|
5406
|
+
...(typeof body.temperature === 'number' ? { temperature: body.temperature } : {}),
|
|
5407
|
+
});
|
|
5408
|
+
const shouldPublish = body.status === 'PUBLISHED' || body.publish === true;
|
|
5409
|
+
const finalTitle = body.title ?? result.title;
|
|
5410
|
+
const finalSlug = body.slug ?? result.slug;
|
|
5411
|
+
// Spread AI data FIRST so explicit caller overrides (title, slug) win.
|
|
5412
|
+
// We don't use `pageSettings` here — that's a page-builder-specific
|
|
5413
|
+
// wire shape. For non-page collections SEO copy lives on regular
|
|
5414
|
+
// fields (metaTitle / metaDescription) declared via the seoFields
|
|
5415
|
+
// preset, and the AI generator already populates those inside
|
|
5416
|
+
// result.data when they exist on the schema.
|
|
5417
|
+
const docPayload = {
|
|
5418
|
+
...result.data,
|
|
5419
|
+
title: finalTitle,
|
|
5420
|
+
slug: finalSlug,
|
|
5421
|
+
};
|
|
5422
|
+
const ctx = buildActionContext(auth.session, db());
|
|
5423
|
+
let doc = await createDocument(targetCollection, docPayload, ctx);
|
|
5424
|
+
// Optional publish: createDocument intentionally always returns DRAFT
|
|
5425
|
+
// (only EDITOR+ can publish). If the caller asked for an immediate
|
|
5426
|
+
// publish we do a follow-up updateDocument that runs through the
|
|
5427
|
+
// standard publish access check. Callers without `update` scope on the
|
|
5428
|
+
// collection will get a 403 here even though the create succeeded —
|
|
5429
|
+
// they end up with a draft, which is the safest fallback.
|
|
5430
|
+
if (shouldPublish && doc?.id) {
|
|
5431
|
+
try {
|
|
5432
|
+
doc = await updateDocument(targetCollection, doc.id, { status: 'PUBLISHED' }, ctx);
|
|
5433
|
+
}
|
|
5434
|
+
catch (publishErr) {
|
|
5435
|
+
// Surface a 207-style partial success: the draft was created but
|
|
5436
|
+
// publish was denied. We return 201 with a `warning` field so the
|
|
5437
|
+
// agent knows the doc landed but needs human publishing.
|
|
5438
|
+
return json({
|
|
5439
|
+
data: {
|
|
5440
|
+
document: doc,
|
|
5441
|
+
generation: {
|
|
5442
|
+
usage: result.usage,
|
|
5443
|
+
durationMs: result.durationMs,
|
|
5444
|
+
seo: result.seo,
|
|
5445
|
+
},
|
|
5446
|
+
},
|
|
5447
|
+
warning: `Document created as DRAFT — publish failed: ${publishErr instanceof Error ? publishErr.message : 'unknown'}`,
|
|
5448
|
+
}, 201);
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
await logEvent({
|
|
5452
|
+
event: 'settings_changed',
|
|
5453
|
+
userId: auth.session.userId,
|
|
5454
|
+
details: {
|
|
5455
|
+
action: 'collection_ai_create',
|
|
5456
|
+
collection: targetCollection,
|
|
5457
|
+
documentId: doc?.id,
|
|
5458
|
+
prompt: redactSecrets(body.prompt).slice(0, 500),
|
|
5459
|
+
tokensUsed: result.usage,
|
|
5460
|
+
durationMs: result.durationMs,
|
|
5461
|
+
},
|
|
5462
|
+
});
|
|
5463
|
+
return json({
|
|
5464
|
+
data: {
|
|
5465
|
+
document: doc,
|
|
5466
|
+
generation: {
|
|
5467
|
+
usage: result.usage,
|
|
5468
|
+
durationMs: result.durationMs,
|
|
5469
|
+
seo: result.seo,
|
|
5470
|
+
},
|
|
5471
|
+
},
|
|
5472
|
+
}, 201);
|
|
5473
|
+
}
|
|
5474
|
+
catch (err) {
|
|
5475
|
+
return internalError(err, 'collections/:slug/ai-create');
|
|
5476
|
+
}
|
|
5477
|
+
});
|
|
5336
5478
|
router.post('/page-builder/audit-a11y', async (request) => {
|
|
5337
5479
|
try {
|
|
5338
5480
|
const auth = await requireAuth(request);
|