@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.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
|
-
|
|
423
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
182
424
|
status: "draft",
|
|
183
|
-
|
|
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.
|
|
186
|
-
this.emitter.emit("content:created",
|
|
187
|
-
return
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
238
|
-
|
|
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:
|
|
241
|
-
updatedAt:
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
this.emitter.emit("content:published",
|
|
245
|
-
return
|
|
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.
|
|
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
|
|
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.
|
|
258
|
-
|
|
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.
|
|
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
|
|
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.
|
|
272
|
-
|
|
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
|
-
|
|
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(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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.
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
677
|
+
return Array.from(this.cache.values());
|
|
356
678
|
}
|
|
357
679
|
async update(id, input) {
|
|
358
|
-
const existing = this.
|
|
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 = {
|
|
363
|
-
|
|
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.
|
|
698
|
+
if (!this.cache.has(id)) {
|
|
368
699
|
throw new NotFoundError(`Workflow not found: ${id}`);
|
|
369
700
|
}
|
|
370
|
-
this.
|
|
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.
|
|
1219
|
+
const workflowDefaults = this._options.workflows?.defaults ?? {};
|
|
526
1220
|
this.contentTypes = new ContentTypesNamespaceImpl();
|
|
527
|
-
this.
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
WorkflowFactory,
|
|
595
|
-
WorkflowTemplates,
|
|
596
|
-
createContentManagementSuite
|
|
1334
|
+
createContentManagementSuite,
|
|
1335
|
+
createInMemoryAdapter
|
|
597
1336
|
};
|
|
598
1337
|
//# sourceMappingURL=index.mjs.map
|