@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.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
- VideoContentType: () => import_content_type_video.VideoContentType,
49
- WorkflowBuilder: () => import_content_editorial_workflow.WorkflowBuilder,
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
- data: { ...input.data },
468
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
237
469
  status: "draft",
238
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
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.store.set(item.id, item);
241
- this.emitter.emit("content:created", item);
242
- return item;
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.store.get(id);
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
- let items = Array.from(this.store.values());
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.store.get(id);
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 updated = {
272
- ...existing,
273
- data: { ...existing.data, ...input.data },
274
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
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
- this.store.set(id, updated);
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
- if (!this.store.has(id)) {
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.store.delete(id);
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.store.get(id);
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
- const published = {
293
- ...existing,
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: (/* @__PURE__ */ new Date()).toISOString(),
296
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
297
- };
298
- this.store.set(id, published);
299
- this.emitter.emit("content:published", published);
300
- return published;
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.store.get(id);
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 unpublished = {
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.store.set(id, unpublished);
313
- this.emitter.emit("content:unpublished", unpublished);
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.store.get(id);
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 scheduled = {
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.store.set(id, scheduled);
327
- this.emitter.emit("content:scheduled", { item: scheduled, date });
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
- const lowerQuery = query.toLowerCase();
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(contentType) {
358
- const id = contentType.id ?? contentType.name;
359
- if (!id) {
360
- throw new Error("Content type must have an id or name");
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
- const contentType = this.store.get(id);
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.entries()).map(([id, contentType]) => ({
379
- id,
380
- contentType
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.store = /* @__PURE__ */ new Map();
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.store.set(workflow.id, workflow);
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.store.get(id);
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.store.values());
722
+ return Array.from(this.cache.values());
411
723
  }
412
724
  async update(id, input) {
413
- const existing = this.store.get(id);
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 = { ...existing, ...input };
418
- this.store.set(id, updated);
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.store.has(id)) {
743
+ if (!this.cache.has(id)) {
423
744
  throw new NotFoundError(`Workflow not found: ${id}`);
424
745
  }
425
- this.store.delete(id);
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.content = new ContentNamespaceImpl(this);
1264
+ const workflowDefaults = this._options.workflows?.defaults ?? {};
581
1265
  this.contentTypes = new ContentTypesNamespaceImpl();
582
- this.workflows = new WorkflowsNamespaceImpl();
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
- VideoContentType,
649
- WorkflowBuilder,
650
- WorkflowFactory,
651
- WorkflowTemplates,
652
- createContentManagementSuite
1380
+ createContentManagementSuite,
1381
+ createInMemoryAdapter
653
1382
  });
654
1383
  //# sourceMappingURL=index.js.map