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