@bernierllc/content-management-suite 0.6.0 → 0.7.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/index.d.mts +6 -883
- package/dist/index.d.ts +6 -883
- package/dist/index.js +860 -131
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +858 -119
- package/dist/index.mjs.map +1 -1
- package/dist/prisma.d.mts +26 -0
- package/dist/prisma.d.ts +26 -0
- package/dist/prisma.js +35 -0
- package/dist/prisma.js.map +1 -0
- package/dist/prisma.mjs +10 -0
- package/dist/prisma.mjs.map +1 -0
- package/dist/types-DQpwJ5e3.d.ts +1234 -0
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -30,26 +30,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
AudioContentTypeManager: () => import_content_type_audio.AudioContentTypeManager,
|
|
34
|
-
AutosaveManager: () => import_content_autosave_manager.AutosaveManager,
|
|
35
33
|
ConflictError: () => ConflictError,
|
|
36
34
|
ContentManagementConfigSchema: () => ContentManagementConfigSchema,
|
|
37
35
|
ContentManagementError: () => ContentManagementError,
|
|
38
|
-
ContentSoftDelete: () => import_content_soft_delete.ContentSoftDelete,
|
|
39
|
-
ContentTypeRegistry: () => import_content_type_registry.ContentTypeRegistry,
|
|
40
|
-
EditorialWorkflowEngine: () => import_content_editorial_workflow.EditorialWorkflowEngine,
|
|
41
36
|
ForbiddenError: () => ForbiddenError,
|
|
42
|
-
ImageContentType: () => import_content_type_image.ImageContentType,
|
|
43
37
|
InternalError: () => InternalError,
|
|
44
38
|
NotFoundError: () => NotFoundError,
|
|
45
|
-
TextContentType: () => import_content_type_text.TextContentType,
|
|
46
39
|
UnauthorizedError: () => UnauthorizedError,
|
|
47
40
|
ValidationError: () => ValidationError,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
WorkflowFactory: () => import_content_editorial_workflow.WorkflowFactory,
|
|
51
|
-
WorkflowTemplates: () => import_content_editorial_workflow.WorkflowTemplates,
|
|
52
|
-
createContentManagementSuite: () => createContentManagementSuite
|
|
41
|
+
createContentManagementSuite: () => createContentManagementSuite,
|
|
42
|
+
createInMemoryAdapter: () => createInMemoryAdapter
|
|
53
43
|
});
|
|
54
44
|
module.exports = __toCommonJS(src_exports);
|
|
55
45
|
|
|
@@ -165,6 +155,9 @@ var ContentManagementConfigSchema = import_zod.z.object({
|
|
|
165
155
|
{ name: "user", permissions: ["content.view"], description: "Content viewing only" }
|
|
166
156
|
])
|
|
167
157
|
}).default({}),
|
|
158
|
+
workflows: import_zod.z.object({
|
|
159
|
+
defaults: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({})
|
|
160
|
+
}).default({}),
|
|
168
161
|
logging: import_zod.z.object({
|
|
169
162
|
level: import_zod.z.enum(["error", "warn", "info", "debug"]).default("info")
|
|
170
163
|
}).default({}),
|
|
@@ -222,144 +215,431 @@ var InternalError = class extends ContentManagementError {
|
|
|
222
215
|
}
|
|
223
216
|
};
|
|
224
217
|
|
|
218
|
+
// src/in-memory-adapter.ts
|
|
219
|
+
var InMemoryContentStorage = class {
|
|
220
|
+
constructor() {
|
|
221
|
+
this.store = /* @__PURE__ */ new Map();
|
|
222
|
+
}
|
|
223
|
+
async list(filters) {
|
|
224
|
+
let items = Array.from(this.store.values());
|
|
225
|
+
if (filters?.type) {
|
|
226
|
+
items = items.filter((item) => item.type === filters.type);
|
|
227
|
+
}
|
|
228
|
+
if (filters?.status) {
|
|
229
|
+
items = items.filter((item) => item.status === filters.status);
|
|
230
|
+
}
|
|
231
|
+
const total = items.length;
|
|
232
|
+
const page = filters?.page ?? 1;
|
|
233
|
+
const limit = filters?.limit ?? items.length;
|
|
234
|
+
const start = (page - 1) * limit;
|
|
235
|
+
const paged = items.slice(start, start + limit);
|
|
236
|
+
return { items: paged, total, page, limit };
|
|
237
|
+
}
|
|
238
|
+
async get(id) {
|
|
239
|
+
return this.store.get(id) ?? null;
|
|
240
|
+
}
|
|
241
|
+
async create(item) {
|
|
242
|
+
this.store.set(item.id, item);
|
|
243
|
+
return item;
|
|
244
|
+
}
|
|
245
|
+
async update(id, partial) {
|
|
246
|
+
const existing = this.store.get(id);
|
|
247
|
+
if (!existing) {
|
|
248
|
+
throw new Error(`Content item not found: ${id}`);
|
|
249
|
+
}
|
|
250
|
+
const updated = { ...existing, ...partial };
|
|
251
|
+
this.store.set(id, updated);
|
|
252
|
+
return updated;
|
|
253
|
+
}
|
|
254
|
+
async delete(id) {
|
|
255
|
+
this.store.delete(id);
|
|
256
|
+
}
|
|
257
|
+
async findBySource(sourceType, sourceId) {
|
|
258
|
+
for (const item of this.store.values()) {
|
|
259
|
+
if (item.sourceType === sourceType && item.sourceId === sourceId) {
|
|
260
|
+
return item;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
async search(query, options) {
|
|
266
|
+
const lowerQuery = query.toLowerCase();
|
|
267
|
+
let items = Array.from(this.store.values()).filter((item) => {
|
|
268
|
+
const titleMatch = item.title && item.title.toLowerCase().includes(lowerQuery);
|
|
269
|
+
const bodyMatch = item.body && item.body.toLowerCase().includes(lowerQuery);
|
|
270
|
+
const dataMatch = item.data && Object.values(item.data).some(
|
|
271
|
+
(value) => typeof value === "string" && value.toLowerCase().includes(lowerQuery)
|
|
272
|
+
);
|
|
273
|
+
return titleMatch || bodyMatch || dataMatch;
|
|
274
|
+
});
|
|
275
|
+
if (options?.type) {
|
|
276
|
+
items = items.filter((item) => item.type === options.type);
|
|
277
|
+
}
|
|
278
|
+
if (options?.status) {
|
|
279
|
+
items = items.filter((item) => item.status === options.status);
|
|
280
|
+
}
|
|
281
|
+
const total = items.length;
|
|
282
|
+
const page = options?.page ?? 1;
|
|
283
|
+
const limit = options?.limit ?? items.length;
|
|
284
|
+
const start = (page - 1) * limit;
|
|
285
|
+
const paged = items.slice(start, start + limit);
|
|
286
|
+
return { items: paged, total, page, limit };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var InMemoryWorkflowStorage = class {
|
|
290
|
+
constructor() {
|
|
291
|
+
this.store = /* @__PURE__ */ new Map();
|
|
292
|
+
this.contentTypeMap = /* @__PURE__ */ new Map();
|
|
293
|
+
}
|
|
294
|
+
// contentType -> workflowId
|
|
295
|
+
async list() {
|
|
296
|
+
return Array.from(this.store.values());
|
|
297
|
+
}
|
|
298
|
+
async get(id) {
|
|
299
|
+
return this.store.get(id) ?? null;
|
|
300
|
+
}
|
|
301
|
+
async create(workflow) {
|
|
302
|
+
this.store.set(workflow.id, workflow);
|
|
303
|
+
return workflow;
|
|
304
|
+
}
|
|
305
|
+
async update(id, partial) {
|
|
306
|
+
const existing = this.store.get(id);
|
|
307
|
+
if (!existing) {
|
|
308
|
+
throw new Error(`Workflow not found: ${id}`);
|
|
309
|
+
}
|
|
310
|
+
const updated = { ...existing, ...partial };
|
|
311
|
+
this.store.set(id, updated);
|
|
312
|
+
return updated;
|
|
313
|
+
}
|
|
314
|
+
async delete(id) {
|
|
315
|
+
this.store.delete(id);
|
|
316
|
+
for (const [ct, wId] of this.contentTypeMap.entries()) {
|
|
317
|
+
if (wId === id) {
|
|
318
|
+
this.contentTypeMap.delete(ct);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async findByContentType(contentType) {
|
|
323
|
+
const workflowId = this.contentTypeMap.get(contentType);
|
|
324
|
+
if (!workflowId) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return this.store.get(workflowId) ?? null;
|
|
328
|
+
}
|
|
329
|
+
setContentTypeMapping(contentType, workflowId) {
|
|
330
|
+
this.contentTypeMap.set(contentType, workflowId);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var InMemoryContentTypeStorage = class {
|
|
334
|
+
constructor() {
|
|
335
|
+
this.store = /* @__PURE__ */ new Map();
|
|
336
|
+
}
|
|
337
|
+
async list() {
|
|
338
|
+
return Array.from(this.store.values());
|
|
339
|
+
}
|
|
340
|
+
async get(id) {
|
|
341
|
+
return this.store.get(id) ?? null;
|
|
342
|
+
}
|
|
343
|
+
async create(def) {
|
|
344
|
+
this.store.set(def.id, def);
|
|
345
|
+
return def;
|
|
346
|
+
}
|
|
347
|
+
async update(id, partial) {
|
|
348
|
+
const existing = this.store.get(id);
|
|
349
|
+
if (!existing) {
|
|
350
|
+
throw new Error(`Content type not found: ${id}`);
|
|
351
|
+
}
|
|
352
|
+
const updated = { ...existing, ...partial };
|
|
353
|
+
this.store.set(id, updated);
|
|
354
|
+
return updated;
|
|
355
|
+
}
|
|
356
|
+
async delete(id) {
|
|
357
|
+
this.store.delete(id);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var InMemorySocialPostStorage = class {
|
|
361
|
+
constructor() {
|
|
362
|
+
this.store = /* @__PURE__ */ new Map();
|
|
363
|
+
}
|
|
364
|
+
async list(filters) {
|
|
365
|
+
let items = Array.from(this.store.values());
|
|
366
|
+
if (filters?.contentId) {
|
|
367
|
+
items = items.filter((p) => p.contentId === filters.contentId);
|
|
368
|
+
}
|
|
369
|
+
if (filters?.platform) {
|
|
370
|
+
items = items.filter((p) => p.platform === filters.platform);
|
|
371
|
+
}
|
|
372
|
+
if (filters?.status) {
|
|
373
|
+
items = items.filter((p) => p.status === filters.status);
|
|
374
|
+
}
|
|
375
|
+
return items;
|
|
376
|
+
}
|
|
377
|
+
async get(id) {
|
|
378
|
+
return this.store.get(id) ?? null;
|
|
379
|
+
}
|
|
380
|
+
async create(post) {
|
|
381
|
+
this.store.set(post.id, post);
|
|
382
|
+
return post;
|
|
383
|
+
}
|
|
384
|
+
async update(id, partial) {
|
|
385
|
+
const existing = this.store.get(id);
|
|
386
|
+
if (!existing) {
|
|
387
|
+
throw new Error(`Social post not found: ${id}`);
|
|
388
|
+
}
|
|
389
|
+
const updated = { ...existing, ...partial };
|
|
390
|
+
this.store.set(id, updated);
|
|
391
|
+
return updated;
|
|
392
|
+
}
|
|
393
|
+
async delete(id) {
|
|
394
|
+
this.store.delete(id);
|
|
395
|
+
}
|
|
396
|
+
async findByContent(contentId) {
|
|
397
|
+
return Array.from(this.store.values()).filter((p) => p.contentId === contentId);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var InMemoryAIReviewStorage = class {
|
|
401
|
+
constructor() {
|
|
402
|
+
this.store = /* @__PURE__ */ new Map();
|
|
403
|
+
}
|
|
404
|
+
async list(filters) {
|
|
405
|
+
let items = Array.from(this.store.values());
|
|
406
|
+
if (filters?.contentId) {
|
|
407
|
+
items = items.filter((r) => r.contentId === filters.contentId);
|
|
408
|
+
}
|
|
409
|
+
return items;
|
|
410
|
+
}
|
|
411
|
+
async get(id) {
|
|
412
|
+
return this.store.get(id) ?? null;
|
|
413
|
+
}
|
|
414
|
+
async create(review) {
|
|
415
|
+
this.store.set(review.id, review);
|
|
416
|
+
return review;
|
|
417
|
+
}
|
|
418
|
+
async findByContent(contentId) {
|
|
419
|
+
return Array.from(this.store.values()).filter((r) => r.contentId === contentId);
|
|
420
|
+
}
|
|
421
|
+
async getLatestForContent(contentId) {
|
|
422
|
+
const reviews = await this.findByContent(contentId);
|
|
423
|
+
if (reviews.length === 0) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
reviews.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
427
|
+
return reviews[0] ?? null;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
function createInMemoryAdapter() {
|
|
431
|
+
return {
|
|
432
|
+
content: new InMemoryContentStorage(),
|
|
433
|
+
workflows: new InMemoryWorkflowStorage(),
|
|
434
|
+
contentTypes: new InMemoryContentTypeStorage(),
|
|
435
|
+
socialPosts: new InMemorySocialPostStorage(),
|
|
436
|
+
aiReviews: new InMemoryAIReviewStorage()
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
225
440
|
// src/namespaces/content.ts
|
|
226
441
|
var crypto = __toESM(require("crypto"));
|
|
227
442
|
var ContentNamespaceImpl = class {
|
|
228
|
-
constructor(emitter) {
|
|
229
|
-
this.store = /* @__PURE__ */ new Map();
|
|
443
|
+
constructor(emitter, storage, getContentTypes, getPublishers) {
|
|
230
444
|
this.emitter = emitter;
|
|
445
|
+
this.storage = storage;
|
|
446
|
+
this.getContentTypes = getContentTypes;
|
|
447
|
+
this.getPublishers = getPublishers;
|
|
231
448
|
}
|
|
232
449
|
async create(input) {
|
|
450
|
+
const contentTypes = this.getContentTypes();
|
|
451
|
+
const typeDef = contentTypes.get(input.type);
|
|
452
|
+
if (typeDef && typeDef.schema) {
|
|
453
|
+
const dataToValidate = {
|
|
454
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
455
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
456
|
+
...input.data ?? {}
|
|
457
|
+
};
|
|
458
|
+
const result = contentTypes.validate(input.type, dataToValidate);
|
|
459
|
+
if (!result.valid) {
|
|
460
|
+
throw new ValidationError("Content type schema validation failed", {
|
|
461
|
+
details: { errors: result.errors }
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
233
465
|
const item = {
|
|
234
466
|
id: crypto.randomUUID(),
|
|
235
467
|
type: input.type,
|
|
236
|
-
|
|
468
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
237
469
|
status: "draft",
|
|
238
|
-
|
|
470
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
471
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
472
|
+
...input.data !== void 0 ? { data: { ...input.data } } : {},
|
|
473
|
+
...input.channels !== void 0 ? { channels: input.channels } : {},
|
|
474
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
239
475
|
};
|
|
240
|
-
this.
|
|
241
|
-
this.emitter.emit("content:created",
|
|
242
|
-
return
|
|
476
|
+
const created = await this.storage.content.create(item);
|
|
477
|
+
this.emitter.emit("content:created", created);
|
|
478
|
+
return created;
|
|
243
479
|
}
|
|
244
480
|
async get(id) {
|
|
245
|
-
const item = this.
|
|
481
|
+
const item = await this.storage.content.get(id);
|
|
246
482
|
if (!item) {
|
|
247
483
|
throw new NotFoundError(`Content item not found: ${id}`);
|
|
248
484
|
}
|
|
249
485
|
return item;
|
|
250
486
|
}
|
|
251
487
|
async list(filters) {
|
|
252
|
-
|
|
253
|
-
if (filters?.type) {
|
|
254
|
-
items = items.filter((item) => item.type === filters.type);
|
|
255
|
-
}
|
|
256
|
-
if (filters?.status) {
|
|
257
|
-
items = items.filter((item) => item.status === filters.status);
|
|
258
|
-
}
|
|
259
|
-
const total = items.length;
|
|
260
|
-
const page = filters?.page ?? 1;
|
|
261
|
-
const limit = filters?.limit ?? items.length;
|
|
262
|
-
const start = (page - 1) * limit;
|
|
263
|
-
const paged = items.slice(start, start + limit);
|
|
264
|
-
return { items: paged, total, page, limit };
|
|
488
|
+
return this.storage.content.list(filters);
|
|
265
489
|
}
|
|
266
490
|
async update(id, input) {
|
|
267
|
-
const existing = this.
|
|
491
|
+
const existing = await this.storage.content.get(id);
|
|
268
492
|
if (!existing) {
|
|
269
493
|
throw new NotFoundError(`Content item not found: ${id}`);
|
|
270
494
|
}
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
495
|
+
const partial = {
|
|
496
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
497
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
498
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
499
|
+
...input.status !== void 0 ? { status: input.status } : {},
|
|
500
|
+
...input.channels !== void 0 ? { channels: input.channels } : {},
|
|
501
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
275
502
|
};
|
|
276
|
-
|
|
503
|
+
if (input.data !== void 0) {
|
|
504
|
+
partial.data = { ...existing.data ?? {}, ...input.data };
|
|
505
|
+
}
|
|
506
|
+
const updated = await this.storage.content.update(id, partial);
|
|
277
507
|
this.emitter.emit("content:updated", updated);
|
|
278
508
|
return updated;
|
|
279
509
|
}
|
|
280
510
|
async delete(id) {
|
|
281
|
-
|
|
511
|
+
const existing = await this.storage.content.get(id);
|
|
512
|
+
if (!existing) {
|
|
282
513
|
throw new NotFoundError(`Content item not found: ${id}`);
|
|
283
514
|
}
|
|
284
|
-
this.
|
|
515
|
+
await this.storage.content.delete(id);
|
|
285
516
|
this.emitter.emit("content:deleted", { id });
|
|
286
517
|
}
|
|
287
|
-
async publish(id) {
|
|
288
|
-
const existing = this.
|
|
518
|
+
async publish(id, options) {
|
|
519
|
+
const existing = await this.storage.content.get(id);
|
|
289
520
|
if (!existing) {
|
|
290
521
|
throw new NotFoundError(`Content item not found: ${id}`);
|
|
291
522
|
}
|
|
292
|
-
|
|
293
|
-
|
|
523
|
+
if (existing.workflowId) {
|
|
524
|
+
const workflow = await this.storage.workflows.get(existing.workflowId);
|
|
525
|
+
if (workflow && workflow.stages && workflow.stages.length > 0) {
|
|
526
|
+
const currentStage = workflow.stages.find((s) => s.id === existing.workflowStage);
|
|
527
|
+
const publishStage = workflow.stages.find((s) => s.isPublishStage);
|
|
528
|
+
if (publishStage && (!currentStage || !currentStage.isPublishStage)) {
|
|
529
|
+
throw new ValidationError(
|
|
530
|
+
`Content is not at a publish-eligible stage. Current stage: "${existing.workflowStage ?? "none"}", required publish stage: "${publishStage.id}"`,
|
|
531
|
+
{
|
|
532
|
+
details: {
|
|
533
|
+
currentStage: existing.workflowStage ?? null,
|
|
534
|
+
requiredStage: publishStage.id
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const publishers = this.getPublishers();
|
|
542
|
+
const allPublishers = publishers.list();
|
|
543
|
+
const channelIds = options?.channels ?? existing.channels;
|
|
544
|
+
let targetPublishers;
|
|
545
|
+
if (channelIds && channelIds.length > 0) {
|
|
546
|
+
targetPublishers = allPublishers.filter((p) => channelIds.includes(p.id));
|
|
547
|
+
} else {
|
|
548
|
+
targetPublishers = allPublishers.filter((p) => p.acceptedTypes.includes(existing.type));
|
|
549
|
+
}
|
|
550
|
+
const results = [];
|
|
551
|
+
for (const publisher of targetPublishers) {
|
|
552
|
+
if (!publisher.acceptedTypes.includes(existing.type)) {
|
|
553
|
+
results.push({
|
|
554
|
+
channelId: publisher.id,
|
|
555
|
+
success: false,
|
|
556
|
+
error: `Content type "${existing.type}" not accepted by publisher "${publisher.id}". Accepted: ${publisher.acceptedTypes.join(", ")}`
|
|
557
|
+
});
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (publisher.validate) {
|
|
561
|
+
const validationErrors = await publisher.validate(existing);
|
|
562
|
+
if (validationErrors.length > 0) {
|
|
563
|
+
results.push({
|
|
564
|
+
channelId: publisher.id,
|
|
565
|
+
success: false,
|
|
566
|
+
error: `Validation failed: ${validationErrors.map((e) => e.message).join(", ")}`
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const result = await publisher.publish(existing);
|
|
573
|
+
results.push(result);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
results.push({
|
|
576
|
+
channelId: publisher.id,
|
|
577
|
+
success: false,
|
|
578
|
+
error: error instanceof Error ? error.message : String(error)
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
583
|
+
const updated = await this.storage.content.update(id, {
|
|
294
584
|
status: "published",
|
|
295
|
-
publishedAt:
|
|
296
|
-
updatedAt:
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
this.emitter.emit("content:published",
|
|
300
|
-
return
|
|
585
|
+
publishedAt: now,
|
|
586
|
+
updatedAt: now
|
|
587
|
+
});
|
|
588
|
+
const publishResult = { item: updated, results };
|
|
589
|
+
this.emitter.emit("content:published", publishResult);
|
|
590
|
+
return publishResult;
|
|
301
591
|
}
|
|
302
592
|
async unpublish(id) {
|
|
303
|
-
const existing = this.
|
|
593
|
+
const existing = await this.storage.content.get(id);
|
|
304
594
|
if (!existing) {
|
|
305
595
|
throw new NotFoundError(`Content item not found: ${id}`);
|
|
306
596
|
}
|
|
307
|
-
const
|
|
308
|
-
...existing,
|
|
597
|
+
const updated = await this.storage.content.update(id, {
|
|
309
598
|
status: "draft",
|
|
310
599
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
311
|
-
};
|
|
312
|
-
this.
|
|
313
|
-
|
|
314
|
-
return unpublished;
|
|
600
|
+
});
|
|
601
|
+
this.emitter.emit("content:unpublished", updated);
|
|
602
|
+
return updated;
|
|
315
603
|
}
|
|
316
604
|
async schedule(id, date) {
|
|
317
|
-
const existing = this.
|
|
605
|
+
const existing = await this.storage.content.get(id);
|
|
318
606
|
if (!existing) {
|
|
319
607
|
throw new NotFoundError(`Content item not found: ${id}`);
|
|
320
608
|
}
|
|
321
|
-
const
|
|
322
|
-
...existing,
|
|
609
|
+
const updated = await this.storage.content.update(id, {
|
|
323
610
|
scheduledFor: date,
|
|
324
611
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
325
|
-
};
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
return scheduled;
|
|
612
|
+
});
|
|
613
|
+
this.emitter.emit("content:scheduled", { item: updated, date });
|
|
614
|
+
return updated;
|
|
329
615
|
}
|
|
330
616
|
async search(query, options) {
|
|
331
|
-
|
|
332
|
-
let items = Array.from(this.store.values()).filter((item) => {
|
|
333
|
-
return Object.values(item.data).some(
|
|
334
|
-
(value) => typeof value === "string" && value.toLowerCase().includes(lowerQuery)
|
|
335
|
-
);
|
|
336
|
-
});
|
|
337
|
-
if (options?.type) {
|
|
338
|
-
items = items.filter((item) => item.type === options.type);
|
|
339
|
-
}
|
|
340
|
-
if (options?.status) {
|
|
341
|
-
items = items.filter((item) => item.status === options.status);
|
|
342
|
-
}
|
|
343
|
-
const total = items.length;
|
|
344
|
-
const page = options?.page ?? 1;
|
|
345
|
-
const limit = options?.limit ?? items.length;
|
|
346
|
-
const start = (page - 1) * limit;
|
|
347
|
-
const paged = items.slice(start, start + limit);
|
|
348
|
-
return { items: paged, total, page, limit };
|
|
617
|
+
return this.storage.content.search(query, options);
|
|
349
618
|
}
|
|
350
619
|
};
|
|
351
620
|
|
|
352
621
|
// src/namespaces/content-types.ts
|
|
622
|
+
var import_zod2 = require("zod");
|
|
353
623
|
var ContentTypesNamespaceImpl = class {
|
|
354
624
|
constructor() {
|
|
355
625
|
this.store = /* @__PURE__ */ new Map();
|
|
356
626
|
}
|
|
357
|
-
register(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
627
|
+
register(definition) {
|
|
628
|
+
if ("schema" in definition && definition.schema) {
|
|
629
|
+
const def = definition;
|
|
630
|
+
this.store.set(def.id, def);
|
|
631
|
+
} else {
|
|
632
|
+
const id = definition.id ?? definition.name;
|
|
633
|
+
if (!id) {
|
|
634
|
+
throw new Error("Content type must have an id or name");
|
|
635
|
+
}
|
|
636
|
+
const name = definition.name ?? definition.id ?? id;
|
|
637
|
+
this.store.set(id, {
|
|
638
|
+
id,
|
|
639
|
+
name,
|
|
640
|
+
schema: null
|
|
641
|
+
});
|
|
361
642
|
}
|
|
362
|
-
this.store.set(id, contentType);
|
|
363
643
|
}
|
|
364
644
|
unregister(id) {
|
|
365
645
|
if (!this.store.has(id)) {
|
|
@@ -368,25 +648,54 @@ var ContentTypesNamespaceImpl = class {
|
|
|
368
648
|
this.store.delete(id);
|
|
369
649
|
}
|
|
370
650
|
get(id) {
|
|
371
|
-
|
|
372
|
-
if (contentType === void 0) {
|
|
373
|
-
throw new NotFoundError(`Content type not found: ${id}`);
|
|
374
|
-
}
|
|
375
|
-
return { id, contentType };
|
|
651
|
+
return this.store.get(id);
|
|
376
652
|
}
|
|
377
653
|
list() {
|
|
378
|
-
return Array.from(this.store.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
654
|
+
return Array.from(this.store.values());
|
|
655
|
+
}
|
|
656
|
+
validate(type, data) {
|
|
657
|
+
const def = this.store.get(type);
|
|
658
|
+
if (!def || !def.schema) {
|
|
659
|
+
return { valid: true };
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const schema = def.schema;
|
|
663
|
+
schema.parse(data);
|
|
664
|
+
return { valid: true };
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (error instanceof import_zod2.z.ZodError) {
|
|
667
|
+
return {
|
|
668
|
+
valid: false,
|
|
669
|
+
errors: error.issues.map((issue) => ({
|
|
670
|
+
message: issue.message,
|
|
671
|
+
path: issue.path.map(String)
|
|
672
|
+
}))
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
valid: false,
|
|
677
|
+
errors: [{ message: error instanceof Error ? error.message : String(error) }]
|
|
678
|
+
};
|
|
679
|
+
}
|
|
382
680
|
}
|
|
383
681
|
};
|
|
384
682
|
|
|
385
683
|
// src/namespaces/workflows.ts
|
|
386
684
|
var crypto2 = __toESM(require("crypto"));
|
|
387
685
|
var WorkflowsNamespaceImpl = class {
|
|
388
|
-
constructor() {
|
|
389
|
-
this.
|
|
686
|
+
constructor(storage) {
|
|
687
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
688
|
+
this.storage = storage;
|
|
689
|
+
}
|
|
690
|
+
/** Load workflows from storage into cache. Called during suite initialization. */
|
|
691
|
+
async loadFromStorage() {
|
|
692
|
+
if (!this.storage) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const workflows = await this.storage.workflows.list();
|
|
696
|
+
for (const w of workflows) {
|
|
697
|
+
this.cache.set(w.id, w);
|
|
698
|
+
}
|
|
390
699
|
}
|
|
391
700
|
async create(input) {
|
|
392
701
|
const workflow = {
|
|
@@ -396,33 +705,56 @@ var WorkflowsNamespaceImpl = class {
|
|
|
396
705
|
...input.stages !== void 0 ? { stages: input.stages } : {},
|
|
397
706
|
...input.transitions !== void 0 ? { transitions: input.transitions } : {}
|
|
398
707
|
};
|
|
399
|
-
this.
|
|
708
|
+
if (this.storage) {
|
|
709
|
+
await this.storage.workflows.create(workflow);
|
|
710
|
+
}
|
|
711
|
+
this.cache.set(workflow.id, workflow);
|
|
400
712
|
return workflow;
|
|
401
713
|
}
|
|
402
714
|
async get(id) {
|
|
403
|
-
const workflow = this.
|
|
715
|
+
const workflow = this.cache.get(id);
|
|
404
716
|
if (!workflow) {
|
|
405
717
|
throw new NotFoundError(`Workflow not found: ${id}`);
|
|
406
718
|
}
|
|
407
719
|
return workflow;
|
|
408
720
|
}
|
|
409
721
|
async list() {
|
|
410
|
-
return Array.from(this.
|
|
722
|
+
return Array.from(this.cache.values());
|
|
411
723
|
}
|
|
412
724
|
async update(id, input) {
|
|
413
|
-
const existing = this.
|
|
725
|
+
const existing = this.cache.get(id);
|
|
414
726
|
if (!existing) {
|
|
415
727
|
throw new NotFoundError(`Workflow not found: ${id}`);
|
|
416
728
|
}
|
|
417
|
-
const updated = {
|
|
418
|
-
|
|
729
|
+
const updated = {
|
|
730
|
+
...existing,
|
|
731
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
732
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
733
|
+
...input.stages !== void 0 ? { stages: input.stages } : {},
|
|
734
|
+
...input.transitions !== void 0 ? { transitions: input.transitions } : {}
|
|
735
|
+
};
|
|
736
|
+
if (this.storage) {
|
|
737
|
+
await this.storage.workflows.update(id, updated);
|
|
738
|
+
}
|
|
739
|
+
this.cache.set(id, updated);
|
|
419
740
|
return updated;
|
|
420
741
|
}
|
|
421
742
|
async delete(id) {
|
|
422
|
-
if (!this.
|
|
743
|
+
if (!this.cache.has(id)) {
|
|
423
744
|
throw new NotFoundError(`Workflow not found: ${id}`);
|
|
424
745
|
}
|
|
425
|
-
this.
|
|
746
|
+
if (this.storage) {
|
|
747
|
+
const contentResult = await this.storage.content.list();
|
|
748
|
+
const assignedContent = contentResult.items.filter((item) => item.workflowId === id);
|
|
749
|
+
if (assignedContent.length > 0) {
|
|
750
|
+
throw new ConflictError(
|
|
751
|
+
`Cannot delete workflow "${id}": ${assignedContent.length} content item(s) are assigned to it. Reassign or archive them first.`,
|
|
752
|
+
{ details: { assignedCount: assignedContent.length } }
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
await this.storage.workflows.delete(id);
|
|
756
|
+
}
|
|
757
|
+
this.cache.delete(id);
|
|
426
758
|
}
|
|
427
759
|
};
|
|
428
760
|
|
|
@@ -567,6 +899,357 @@ var PluginsNamespaceImpl = class {
|
|
|
567
899
|
}
|
|
568
900
|
};
|
|
569
901
|
|
|
902
|
+
// src/namespaces/publishers.ts
|
|
903
|
+
var WebsitePublisher = class {
|
|
904
|
+
constructor() {
|
|
905
|
+
this.id = "website";
|
|
906
|
+
this.name = "Website";
|
|
907
|
+
this.acceptedTypes = ["blog-post", "article", "page", "generic", "text"];
|
|
908
|
+
}
|
|
909
|
+
async publish(content) {
|
|
910
|
+
const slug = content.title ? content.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : content.id;
|
|
911
|
+
return {
|
|
912
|
+
channelId: this.id,
|
|
913
|
+
success: true,
|
|
914
|
+
url: `/content/${slug}`
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
var RSSPublisher = class {
|
|
919
|
+
constructor() {
|
|
920
|
+
this.id = "rss";
|
|
921
|
+
this.name = "RSS Feed";
|
|
922
|
+
this.acceptedTypes = ["blog-post", "article", "rss-article", "generic", "text"];
|
|
923
|
+
}
|
|
924
|
+
async publish(content) {
|
|
925
|
+
return {
|
|
926
|
+
channelId: this.id,
|
|
927
|
+
success: true,
|
|
928
|
+
url: `/feed/rss/${content.id}`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
var PublishersNamespaceImpl = class {
|
|
933
|
+
constructor(emitter) {
|
|
934
|
+
this.store = /* @__PURE__ */ new Map();
|
|
935
|
+
this.emitter = emitter;
|
|
936
|
+
this.store.set("website", new WebsitePublisher());
|
|
937
|
+
this.store.set("rss", new RSSPublisher());
|
|
938
|
+
}
|
|
939
|
+
register(publisher) {
|
|
940
|
+
this.store.set(publisher.id, publisher);
|
|
941
|
+
this.emitter.emit("publisher:registered", { publisher });
|
|
942
|
+
}
|
|
943
|
+
unregister(id) {
|
|
944
|
+
if (!this.store.has(id)) {
|
|
945
|
+
throw new NotFoundError(`Publisher not found: ${id}`);
|
|
946
|
+
}
|
|
947
|
+
this.store.delete(id);
|
|
948
|
+
}
|
|
949
|
+
list() {
|
|
950
|
+
return Array.from(this.store.values());
|
|
951
|
+
}
|
|
952
|
+
get(id) {
|
|
953
|
+
return this.store.get(id);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/namespaces/sources.ts
|
|
958
|
+
var crypto4 = __toESM(require("crypto"));
|
|
959
|
+
var SourcesNamespaceImpl = class {
|
|
960
|
+
constructor(emitter, storage, workflowDefaults) {
|
|
961
|
+
this.store = /* @__PURE__ */ new Map();
|
|
962
|
+
this.emitter = emitter;
|
|
963
|
+
this.storage = storage;
|
|
964
|
+
this.workflowDefaults = workflowDefaults;
|
|
965
|
+
}
|
|
966
|
+
register(source) {
|
|
967
|
+
this.store.set(source.id, source);
|
|
968
|
+
this.emitter.emit("source:registered", { source });
|
|
969
|
+
}
|
|
970
|
+
unregister(id) {
|
|
971
|
+
if (!this.store.has(id)) {
|
|
972
|
+
throw new NotFoundError(`Source not found: ${id}`);
|
|
973
|
+
}
|
|
974
|
+
this.store.delete(id);
|
|
975
|
+
}
|
|
976
|
+
list() {
|
|
977
|
+
return Array.from(this.store.values());
|
|
978
|
+
}
|
|
979
|
+
get(id) {
|
|
980
|
+
return this.store.get(id);
|
|
981
|
+
}
|
|
982
|
+
async ingest(sourceId, rawPayload) {
|
|
983
|
+
const source = this.store.get(sourceId);
|
|
984
|
+
if (!source) {
|
|
985
|
+
throw new NotFoundError(`Source not found: ${sourceId}`);
|
|
986
|
+
}
|
|
987
|
+
if (source.validate) {
|
|
988
|
+
const validationErrors = await source.validate(rawPayload);
|
|
989
|
+
if (validationErrors.length > 0) {
|
|
990
|
+
throw new ValidationError("Source validation failed", {
|
|
991
|
+
details: { errors: validationErrors }
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const rawItem = await source.ingest(rawPayload);
|
|
996
|
+
if (rawItem.sourceId) {
|
|
997
|
+
const existing = await this.storage.content.findBySource(source.id, rawItem.sourceId);
|
|
998
|
+
if (existing) {
|
|
999
|
+
return { created: false, content: existing };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1003
|
+
const workflowId = this.workflowDefaults[rawItem.type] ?? this.workflowDefaults["generic"];
|
|
1004
|
+
const item = {
|
|
1005
|
+
id: crypto4.randomUUID(),
|
|
1006
|
+
type: rawItem.type,
|
|
1007
|
+
createdAt: now,
|
|
1008
|
+
status: "review",
|
|
1009
|
+
sourceType: source.id,
|
|
1010
|
+
sourceId: rawItem.sourceId ?? null,
|
|
1011
|
+
...rawItem.title !== void 0 ? { title: rawItem.title } : {},
|
|
1012
|
+
...rawItem.body !== void 0 ? { body: rawItem.body } : {},
|
|
1013
|
+
...rawItem.data !== void 0 ? { data: rawItem.data } : {},
|
|
1014
|
+
...rawItem.metadata !== void 0 ? { metadata: rawItem.metadata } : {},
|
|
1015
|
+
...workflowId !== void 0 ? { workflowId } : {}
|
|
1016
|
+
};
|
|
1017
|
+
const created = await this.storage.content.create(item);
|
|
1018
|
+
this.emitter.emit("content:ingested", { item: created, sourceType: source.id });
|
|
1019
|
+
this.emitter.emit("content:created", created);
|
|
1020
|
+
return { created: true, content: created };
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
// src/namespaces/ai.ts
|
|
1025
|
+
var crypto5 = __toESM(require("crypto"));
|
|
1026
|
+
var AINamespaceImpl = class {
|
|
1027
|
+
constructor(emitter, storage, options) {
|
|
1028
|
+
this.emitter = emitter;
|
|
1029
|
+
this.storage = storage;
|
|
1030
|
+
this.reviewService = options.reviewService;
|
|
1031
|
+
this.socialGeneratorService = options.socialGeneratorService;
|
|
1032
|
+
this.enhanceService = options.enhanceService;
|
|
1033
|
+
this.workflowDefaults = options.workflowDefaults ?? {};
|
|
1034
|
+
}
|
|
1035
|
+
async review(contentId) {
|
|
1036
|
+
if (!this.reviewService) {
|
|
1037
|
+
throw new InternalError("AI review service not configured");
|
|
1038
|
+
}
|
|
1039
|
+
const content = await this.storage.content.get(contentId);
|
|
1040
|
+
if (!content) {
|
|
1041
|
+
throw new NotFoundError(`Content item not found: ${contentId}`);
|
|
1042
|
+
}
|
|
1043
|
+
const textToReview = [content.title, content.body].filter(Boolean).join("\n\n") || JSON.stringify(content.data ?? {});
|
|
1044
|
+
const result = await this.reviewService.review(textToReview);
|
|
1045
|
+
const suggestions = result.issues.map((issue) => ({
|
|
1046
|
+
category: issue.category,
|
|
1047
|
+
severity: issue.severity === "info" || issue.severity === "warning" || issue.severity === "error" ? issue.severity : "info",
|
|
1048
|
+
message: issue.message,
|
|
1049
|
+
...issue.originalText !== void 0 ? { originalText: issue.originalText } : {},
|
|
1050
|
+
...issue.suggestedFix !== void 0 ? { suggestedFix: issue.suggestedFix } : {}
|
|
1051
|
+
}));
|
|
1052
|
+
const review = {
|
|
1053
|
+
id: crypto5.randomUUID(),
|
|
1054
|
+
contentId,
|
|
1055
|
+
score: result.overallScore,
|
|
1056
|
+
suggestions,
|
|
1057
|
+
passesThreshold: result.passesThreshold,
|
|
1058
|
+
rawResult: result.raw,
|
|
1059
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1060
|
+
};
|
|
1061
|
+
await this.storage.aiReviews.create(review);
|
|
1062
|
+
this.emitter.emit("ai:review:completed", { contentId, review });
|
|
1063
|
+
return review;
|
|
1064
|
+
}
|
|
1065
|
+
async generateSocialPosts(contentId, platforms) {
|
|
1066
|
+
if (!this.socialGeneratorService) {
|
|
1067
|
+
throw new InternalError("AI social generator service not configured");
|
|
1068
|
+
}
|
|
1069
|
+
const content = await this.storage.content.get(contentId);
|
|
1070
|
+
if (!content) {
|
|
1071
|
+
throw new NotFoundError(`Content item not found: ${contentId}`);
|
|
1072
|
+
}
|
|
1073
|
+
const textContent = [content.title, content.body].filter(Boolean).join("\n\n") || JSON.stringify(content.data ?? {});
|
|
1074
|
+
const generated = await this.socialGeneratorService.generate(textContent, platforms);
|
|
1075
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1076
|
+
const workflowId = this.workflowDefaults["social-post"] ?? this.workflowDefaults["generic"];
|
|
1077
|
+
const posts = generated.map((g) => ({
|
|
1078
|
+
id: crypto5.randomUUID(),
|
|
1079
|
+
contentId,
|
|
1080
|
+
platform: g.platform,
|
|
1081
|
+
body: g.body,
|
|
1082
|
+
status: "draft",
|
|
1083
|
+
platformPostId: null,
|
|
1084
|
+
publishedAt: null,
|
|
1085
|
+
createdAt: now,
|
|
1086
|
+
...workflowId !== void 0 ? { workflowId } : {}
|
|
1087
|
+
}));
|
|
1088
|
+
for (const post of posts) {
|
|
1089
|
+
await this.storage.socialPosts.create(post);
|
|
1090
|
+
}
|
|
1091
|
+
this.emitter.emit("ai:social:generated", { contentId, posts });
|
|
1092
|
+
return posts;
|
|
1093
|
+
}
|
|
1094
|
+
async enhance(contentId, opts) {
|
|
1095
|
+
if (!this.enhanceService) {
|
|
1096
|
+
throw new InternalError("AI enhance service not configured");
|
|
1097
|
+
}
|
|
1098
|
+
const content = await this.storage.content.get(contentId);
|
|
1099
|
+
if (!content) {
|
|
1100
|
+
throw new NotFoundError(`Content item not found: ${contentId}`);
|
|
1101
|
+
}
|
|
1102
|
+
const textContent = [content.title, content.body].filter(Boolean).join("\n\n") || JSON.stringify(content.data ?? {});
|
|
1103
|
+
return this.enhanceService.enhance(textContent, opts);
|
|
1104
|
+
}
|
|
1105
|
+
async suggest(contentId) {
|
|
1106
|
+
if (!this.enhanceService) {
|
|
1107
|
+
throw new InternalError("AI enhance service not configured");
|
|
1108
|
+
}
|
|
1109
|
+
const content = await this.storage.content.get(contentId);
|
|
1110
|
+
if (!content) {
|
|
1111
|
+
throw new NotFoundError(`Content item not found: ${contentId}`);
|
|
1112
|
+
}
|
|
1113
|
+
const textContent = [content.title, content.body].filter(Boolean).join("\n\n") || JSON.stringify(content.data ?? {});
|
|
1114
|
+
return this.enhanceService.suggest(textContent);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/namespaces/social.ts
|
|
1119
|
+
var SocialNamespaceImpl = class {
|
|
1120
|
+
constructor(emitter, storage) {
|
|
1121
|
+
this.store = /* @__PURE__ */ new Map();
|
|
1122
|
+
this.emitter = emitter;
|
|
1123
|
+
this.storage = storage;
|
|
1124
|
+
}
|
|
1125
|
+
registerPlatform(adapter) {
|
|
1126
|
+
this.store.set(adapter.id, adapter);
|
|
1127
|
+
}
|
|
1128
|
+
unregisterPlatform(id) {
|
|
1129
|
+
if (!this.store.has(id)) {
|
|
1130
|
+
throw new NotFoundError(`Social platform not found: ${id}`);
|
|
1131
|
+
}
|
|
1132
|
+
this.store.delete(id);
|
|
1133
|
+
}
|
|
1134
|
+
listPlatforms() {
|
|
1135
|
+
return Array.from(this.store.values()).map((adapter) => ({
|
|
1136
|
+
id: adapter.id,
|
|
1137
|
+
name: adapter.name,
|
|
1138
|
+
acceptedTypes: adapter.acceptedTypes,
|
|
1139
|
+
hasPreview: typeof adapter.preview === "function",
|
|
1140
|
+
hasMetrics: typeof adapter.getMetrics === "function"
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
async publish(contentId, platforms) {
|
|
1144
|
+
const missing = platforms.filter((p) => !this.store.has(p));
|
|
1145
|
+
if (missing.length > 0) {
|
|
1146
|
+
throw new ValidationError(
|
|
1147
|
+
`Unregistered social platform(s): ${missing.join(", ")}`,
|
|
1148
|
+
{ details: { missingPlatforms: missing } }
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
const content = await this.storage.content.get(contentId);
|
|
1152
|
+
if (!content) {
|
|
1153
|
+
throw new NotFoundError(`Content item not found: ${contentId}`);
|
|
1154
|
+
}
|
|
1155
|
+
if (content.workflowId) {
|
|
1156
|
+
const workflow = await this.storage.workflows.get(content.workflowId);
|
|
1157
|
+
if (workflow && workflow.stages && workflow.stages.length > 0) {
|
|
1158
|
+
const currentStage = workflow.stages.find((s) => s.id === content.workflowStage);
|
|
1159
|
+
const publishStage = workflow.stages.find((s) => s.isPublishStage);
|
|
1160
|
+
if (publishStage && (!currentStage || !currentStage.isPublishStage)) {
|
|
1161
|
+
throw new ValidationError(
|
|
1162
|
+
`Content is not at a publish-eligible stage. Current stage: "${content.workflowStage ?? "none"}", required publish stage: "${publishStage.id}"`,
|
|
1163
|
+
{
|
|
1164
|
+
details: {
|
|
1165
|
+
currentStage: content.workflowStage ?? null,
|
|
1166
|
+
requiredStage: publishStage.id
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const socialPosts = await this.storage.socialPosts.findByContent(contentId);
|
|
1174
|
+
const results = [];
|
|
1175
|
+
for (const platformId of platforms) {
|
|
1176
|
+
const adapter = this.store.get(platformId);
|
|
1177
|
+
if (!adapter) {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
if (!adapter.acceptedTypes.includes(content.type)) {
|
|
1181
|
+
results.push({
|
|
1182
|
+
platform: platformId,
|
|
1183
|
+
success: false,
|
|
1184
|
+
error: `Content type "${content.type}" not accepted by platform "${platformId}". Accepted: ${adapter.acceptedTypes.join(", ")}`
|
|
1185
|
+
});
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const post = socialPosts.find((p) => p.platform === platformId && p.status === "draft");
|
|
1189
|
+
if (!post) {
|
|
1190
|
+
results.push({
|
|
1191
|
+
platform: platformId,
|
|
1192
|
+
success: false,
|
|
1193
|
+
error: `No draft social post found for platform "${platformId}"`
|
|
1194
|
+
});
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
const result = await adapter.publish(post);
|
|
1199
|
+
results.push(result);
|
|
1200
|
+
if (result.success) {
|
|
1201
|
+
await this.storage.socialPosts.update(post.id, {
|
|
1202
|
+
status: "published",
|
|
1203
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1204
|
+
...result.platformPostId !== void 0 ? { platformPostId: result.platformPostId } : {}
|
|
1205
|
+
});
|
|
1206
|
+
this.emitter.emit("social:published", { contentId, platform: platformId, result });
|
|
1207
|
+
} else {
|
|
1208
|
+
await this.storage.socialPosts.update(post.id, { status: "failed" });
|
|
1209
|
+
}
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
await this.storage.socialPosts.update(post.id, { status: "failed" });
|
|
1212
|
+
results.push({
|
|
1213
|
+
platform: platformId,
|
|
1214
|
+
success: false,
|
|
1215
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return results;
|
|
1220
|
+
}
|
|
1221
|
+
async preview(contentId, platform) {
|
|
1222
|
+
const adapter = this.store.get(platform);
|
|
1223
|
+
if (!adapter) {
|
|
1224
|
+
throw new NotFoundError(`Social platform not found: ${platform}`);
|
|
1225
|
+
}
|
|
1226
|
+
if (!adapter.preview) {
|
|
1227
|
+
throw new InternalError(`Platform "${platform}" does not support preview`);
|
|
1228
|
+
}
|
|
1229
|
+
const posts = await this.storage.socialPosts.findByContent(contentId);
|
|
1230
|
+
const post = posts.find((p) => p.platform === platform);
|
|
1231
|
+
if (!post) {
|
|
1232
|
+
throw new NotFoundError(`No social post found for content "${contentId}" on platform "${platform}"`);
|
|
1233
|
+
}
|
|
1234
|
+
return adapter.preview(post);
|
|
1235
|
+
}
|
|
1236
|
+
async getMetrics(contentId, platform) {
|
|
1237
|
+
const adapter = this.store.get(platform);
|
|
1238
|
+
if (!adapter) {
|
|
1239
|
+
throw new NotFoundError(`Social platform not found: ${platform}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (!adapter.getMetrics) {
|
|
1242
|
+
throw new InternalError(`Platform "${platform}" does not support metrics`);
|
|
1243
|
+
}
|
|
1244
|
+
const posts = await this.storage.socialPosts.findByContent(contentId);
|
|
1245
|
+
const post = posts.find((p) => p.platform === platform && p.platformPostId);
|
|
1246
|
+
if (!post || !post.platformPostId) {
|
|
1247
|
+
throw new NotFoundError(`No published social post found for content "${contentId}" on platform "${platform}"`);
|
|
1248
|
+
}
|
|
1249
|
+
return adapter.getMetrics(post.platformPostId);
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
|
|
570
1253
|
// src/content-management-suite.ts
|
|
571
1254
|
var ContentManagementSuiteImpl = class extends import_events.EventEmitter {
|
|
572
1255
|
constructor(options) {
|
|
@@ -574,16 +1257,40 @@ var ContentManagementSuiteImpl = class extends import_events.EventEmitter {
|
|
|
574
1257
|
this._initialized = false;
|
|
575
1258
|
this._options = options ?? {};
|
|
576
1259
|
this._logger = this._options.logger;
|
|
1260
|
+
this._storage = this._options.storage ?? createInMemoryAdapter();
|
|
577
1261
|
const parsedConfig = ContentManagementConfigSchema.parse(
|
|
578
1262
|
this._options.config ?? {}
|
|
579
1263
|
);
|
|
580
|
-
this.
|
|
1264
|
+
const workflowDefaults = this._options.workflows?.defaults ?? {};
|
|
581
1265
|
this.contentTypes = new ContentTypesNamespaceImpl();
|
|
582
|
-
this.
|
|
1266
|
+
this.publishers = new PublishersNamespaceImpl(this);
|
|
1267
|
+
this.content = new ContentNamespaceImpl(
|
|
1268
|
+
this,
|
|
1269
|
+
this._storage,
|
|
1270
|
+
() => this.contentTypes,
|
|
1271
|
+
() => this.publishers
|
|
1272
|
+
);
|
|
1273
|
+
this.workflows = new WorkflowsNamespaceImpl(this._storage);
|
|
583
1274
|
this.users = new UsersNamespaceImpl(this);
|
|
584
1275
|
this.permissions = new PermissionsNamespaceImpl();
|
|
585
1276
|
this.config = new ConfigNamespaceImpl(parsedConfig, this);
|
|
586
1277
|
this.plugins = new PluginsNamespaceImpl(this);
|
|
1278
|
+
this.sources = new SourcesNamespaceImpl(this, this._storage, workflowDefaults);
|
|
1279
|
+
const aiOpts = {};
|
|
1280
|
+
if (this._options.ai?.reviewService) {
|
|
1281
|
+
aiOpts.reviewService = this._options.ai.reviewService;
|
|
1282
|
+
}
|
|
1283
|
+
if (this._options.ai?.socialGeneratorService) {
|
|
1284
|
+
aiOpts.socialGeneratorService = this._options.ai.socialGeneratorService;
|
|
1285
|
+
}
|
|
1286
|
+
if (this._options.ai?.enhanceService) {
|
|
1287
|
+
aiOpts.enhanceService = this._options.ai.enhanceService;
|
|
1288
|
+
}
|
|
1289
|
+
if (Object.keys(workflowDefaults).length > 0) {
|
|
1290
|
+
aiOpts.workflowDefaults = workflowDefaults;
|
|
1291
|
+
}
|
|
1292
|
+
this.ai = new AINamespaceImpl(this, this._storage, aiOpts);
|
|
1293
|
+
this.social = new SocialNamespaceImpl(this, this._storage);
|
|
587
1294
|
}
|
|
588
1295
|
async initialize() {
|
|
589
1296
|
if (this._initialized) {
|
|
@@ -593,11 +1300,53 @@ var ContentManagementSuiteImpl = class extends import_events.EventEmitter {
|
|
|
593
1300
|
this.contentTypes.register({ id: "image", name: "image" });
|
|
594
1301
|
this.contentTypes.register({ id: "audio", name: "audio" });
|
|
595
1302
|
this.contentTypes.register({ id: "video", name: "video" });
|
|
1303
|
+
const workflowsImpl = this.workflows;
|
|
1304
|
+
if (typeof workflowsImpl.loadFromStorage === "function") {
|
|
1305
|
+
await workflowsImpl.loadFromStorage();
|
|
1306
|
+
}
|
|
596
1307
|
if (this._options.plugins) {
|
|
597
1308
|
for (const plugin of this._options.plugins) {
|
|
598
1309
|
await this.plugins.register(plugin);
|
|
599
1310
|
}
|
|
600
1311
|
}
|
|
1312
|
+
this.on("ai:review:completed", async ({ contentId, review }) => {
|
|
1313
|
+
try {
|
|
1314
|
+
const content = await this._storage.content.get(contentId);
|
|
1315
|
+
if (!content || !content.workflowId || !content.workflowStage) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const workflow = await this._storage.workflows.get(content.workflowId);
|
|
1319
|
+
if (!workflow || !workflow.stages) {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const currentStage = workflow.stages.find((s) => s.id === content.workflowStage);
|
|
1323
|
+
if (!currentStage || !currentStage.conditions) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const { autoAdvance, minScore, requiresHumanReview } = currentStage.conditions;
|
|
1327
|
+
if (requiresHumanReview) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (autoAdvance && review.passesThreshold && (minScore === void 0 || review.score >= minScore)) {
|
|
1331
|
+
const sortedStages = [...workflow.stages].sort((a, b) => a.order - b.order);
|
|
1332
|
+
const currentIndex = sortedStages.findIndex((s) => s.id === currentStage.id);
|
|
1333
|
+
const nextStage = sortedStages[currentIndex + 1];
|
|
1334
|
+
if (nextStage) {
|
|
1335
|
+
const fromStage = content.workflowStage;
|
|
1336
|
+
await this._storage.content.update(contentId, {
|
|
1337
|
+
workflowStage: nextStage.id
|
|
1338
|
+
});
|
|
1339
|
+
this.emit("workflow:stage:changed", {
|
|
1340
|
+
contentId,
|
|
1341
|
+
workflowId: content.workflowId,
|
|
1342
|
+
from: fromStage,
|
|
1343
|
+
to: nextStage.id
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
601
1350
|
this._initialized = true;
|
|
602
1351
|
this.emit("initialized", void 0);
|
|
603
1352
|
}
|
|
@@ -618,37 +1367,17 @@ var ContentManagementSuiteImpl = class extends import_events.EventEmitter {
|
|
|
618
1367
|
function createContentManagementSuite(options) {
|
|
619
1368
|
return new ContentManagementSuiteImpl(options);
|
|
620
1369
|
}
|
|
621
|
-
|
|
622
|
-
// src/index.ts
|
|
623
|
-
var import_content_type_registry = require("@bernierllc/content-type-registry");
|
|
624
|
-
var import_content_editorial_workflow = require("@bernierllc/content-editorial-workflow");
|
|
625
|
-
var import_content_autosave_manager = require("@bernierllc/content-autosave-manager");
|
|
626
|
-
var import_content_soft_delete = require("@bernierllc/content-soft-delete");
|
|
627
|
-
var import_content_type_text = require("@bernierllc/content-type-text");
|
|
628
|
-
var import_content_type_image = require("@bernierllc/content-type-image");
|
|
629
|
-
var import_content_type_audio = require("@bernierllc/content-type-audio");
|
|
630
|
-
var import_content_type_video = require("@bernierllc/content-type-video");
|
|
631
1370
|
// Annotate the CommonJS export names for ESM import in node:
|
|
632
1371
|
0 && (module.exports = {
|
|
633
|
-
AudioContentTypeManager,
|
|
634
|
-
AutosaveManager,
|
|
635
1372
|
ConflictError,
|
|
636
1373
|
ContentManagementConfigSchema,
|
|
637
1374
|
ContentManagementError,
|
|
638
|
-
ContentSoftDelete,
|
|
639
|
-
ContentTypeRegistry,
|
|
640
|
-
EditorialWorkflowEngine,
|
|
641
1375
|
ForbiddenError,
|
|
642
|
-
ImageContentType,
|
|
643
1376
|
InternalError,
|
|
644
1377
|
NotFoundError,
|
|
645
|
-
TextContentType,
|
|
646
1378
|
UnauthorizedError,
|
|
647
1379
|
ValidationError,
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
WorkflowFactory,
|
|
651
|
-
WorkflowTemplates,
|
|
652
|
-
createContentManagementSuite
|
|
1380
|
+
createContentManagementSuite,
|
|
1381
|
+
createInMemoryAdapter
|
|
653
1382
|
});
|
|
654
1383
|
//# sourceMappingURL=index.js.map
|