@growth-labs/cms 0.8.3 → 0.8.4
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/engine/foundry-dispatch.d.ts.map +1 -1
- package/dist/engine/foundry-dispatch.js +42 -21
- package/dist/engine/foundry-dispatch.js.map +1 -1
- package/dist/engine/fronts-publish-intent.d.ts.map +1 -1
- package/dist/engine/fronts-publish-intent.js +8 -4
- package/dist/engine/fronts-publish-intent.js.map +1 -1
- package/dist/engine/fronts-publish.d.ts.map +1 -1
- package/dist/engine/fronts-publish.js +29 -6
- package/dist/engine/fronts-publish.js.map +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publication-current-guard.d.ts +7 -0
- package/dist/engine/publication-current-guard.d.ts.map +1 -1
- package/dist/engine/publication-current-guard.js +66 -0
- package/dist/engine/publication-current-guard.js.map +1 -1
- package/dist/engine/publication.d.ts +30 -0
- package/dist/engine/publication.d.ts.map +1 -1
- package/dist/engine/publication.js +306 -34
- package/dist/engine/publication.js.map +1 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +16 -11
- package/dist/engine/publisher.js.map +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +19 -4
- package/dist/routes/content.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +42 -42
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +8 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/types.d.ts +6 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/dist/surveys/schema.d.ts +48 -48
- package/migrations/0030_media_publication_guard.sql +5 -0
- package/package.json +1 -1
- package/src/engine/foundry-dispatch.ts +86 -32
- package/src/engine/fronts-publish-intent.ts +8 -4
- package/src/engine/fronts-publish.ts +46 -7
- package/src/engine/index.ts +3 -0
- package/src/engine/publication-current-guard.ts +86 -0
- package/src/engine/publication.ts +411 -59
- package/src/engine/publisher.ts +17 -9
- package/src/routes/content.ts +22 -4
- package/src/schema/migrations.ts +8 -0
- package/src/schema/types.ts +6 -0
package/src/engine/publisher.ts
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
import type { ContentStatus, ContentType, FrontsPublishStatus } from '../schema/types.js'
|
|
23
23
|
import { type ContentMetadata, serializeContentMetadata } from './content-metadata.js'
|
|
24
24
|
import type { D1Database } from './d1.js'
|
|
25
|
-
import { armFrontsPublishTriggerToken } from './fronts-publish-intent.js'
|
|
26
25
|
import { evaluateArticleBody, type PublishGuardOutcome } from './publish-guard.js'
|
|
27
26
|
import { slugify } from './slug.js'
|
|
28
27
|
import {
|
|
@@ -1319,6 +1318,7 @@ export async function updateContentItem(
|
|
|
1319
1318
|
processing_state = CASE WHEN ? THEN 'pending' ELSE processing_state END,
|
|
1320
1319
|
processing_correlation_id = CASE WHEN ? THEN NULL ELSE processing_correlation_id END,
|
|
1321
1320
|
processing_trigger_token = CASE WHEN ? THEN NULL ELSE processing_trigger_token END,
|
|
1321
|
+
processing_publication_guard = CASE WHEN ? THEN NULL ELSE processing_publication_guard END,
|
|
1322
1322
|
processing_started_at = CASE WHEN ? THEN NULL ELSE processing_started_at END,
|
|
1323
1323
|
processing_completed_at = CASE WHEN ? THEN NULL ELSE processing_completed_at END,
|
|
1324
1324
|
processing_last_event_at = CASE WHEN ? THEN NULL ELSE processing_last_event_at END,
|
|
@@ -1353,6 +1353,7 @@ export async function updateContentItem(
|
|
|
1353
1353
|
videoUpdatePlan.sourceChanged ? 1 : 0,
|
|
1354
1354
|
videoUpdatePlan.sourceChanged ? 1 : 0,
|
|
1355
1355
|
videoUpdatePlan.sourceChanged ? 1 : 0,
|
|
1356
|
+
videoUpdatePlan.sourceChanged ? 1 : 0,
|
|
1356
1357
|
id,
|
|
1357
1358
|
options.expectedMediaSource ? 1 : 0,
|
|
1358
1359
|
options.expectedMediaSource?.sourceUrl ?? null,
|
|
@@ -1374,6 +1375,8 @@ export async function updateContentItem(
|
|
|
1374
1375
|
audio_r2_key = ?,
|
|
1375
1376
|
duration_seconds = ?,
|
|
1376
1377
|
processing_trigger_token = ?,
|
|
1378
|
+
processing_publication_guard = CASE WHEN ? IS processing_trigger_token THEN processing_publication_guard ELSE NULL END,
|
|
1379
|
+
processing_state = CASE WHEN ? IS processing_trigger_token THEN processing_state ELSE NULL END,
|
|
1377
1380
|
processing_source_url = ?,
|
|
1378
1381
|
processing_source_kind = ?
|
|
1379
1382
|
WHERE content_id = ?
|
|
@@ -1385,6 +1388,8 @@ export async function updateContentItem(
|
|
|
1385
1388
|
podcastUpdatePlan.audioR2Key,
|
|
1386
1389
|
podcastUpdatePlan.durationSeconds,
|
|
1387
1390
|
podcastUpdatePlan.processingTriggerToken,
|
|
1391
|
+
podcastUpdatePlan.processingTriggerToken,
|
|
1392
|
+
podcastUpdatePlan.processingTriggerToken,
|
|
1388
1393
|
podcastUpdatePlan.sourceUrl,
|
|
1389
1394
|
podcastUpdatePlan.sourceKind,
|
|
1390
1395
|
id,
|
|
@@ -1411,16 +1416,19 @@ export async function scheduleContent(
|
|
|
1411
1416
|
): Promise<void> {
|
|
1412
1417
|
await db
|
|
1413
1418
|
.prepare(
|
|
1414
|
-
`UPDATE content_items SET status = 'scheduled', publish_at = ?, publish_tz = ?,
|
|
1419
|
+
`UPDATE content_items SET status = 'scheduled', publish_at = ?, publish_tz = ?,
|
|
1420
|
+
canonical_version = canonical_version + CASE WHEN status = 'published' OR fronts_publish_status = 'live' THEN 1 ELSE 0 END,
|
|
1421
|
+
fronts_publish_status = CASE WHEN status = 'published' OR fronts_publish_status = 'live'
|
|
1422
|
+
THEN 'queued' ELSE fronts_publish_status END,
|
|
1423
|
+
fronts_publish_trigger_token = CASE WHEN status = 'published' OR fronts_publish_status = 'live'
|
|
1424
|
+
OR fronts_publish_trigger_token IS NULL THEN ? ELSE fronts_publish_trigger_token END,
|
|
1425
|
+
updated_at = unixepoch() WHERE id = ?`,
|
|
1415
1426
|
)
|
|
1416
|
-
.bind(publishAt, publishTz, id)
|
|
1427
|
+
.bind(publishAt, publishTz, crypto.randomUUID(), id)
|
|
1417
1428
|
.run()
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
// `trigger_token_hash` is stable across reschedules and across every poll.
|
|
1422
|
-
// The raw token stays in D1; only `sha256:<hex>` is ever exposed.
|
|
1423
|
-
await armFrontsPublishTriggerToken(db, id)
|
|
1429
|
+
// Slot, terminal-ledger reset and token are one atomic transition. Moving an
|
|
1430
|
+
// already scheduled occurrence preserves its identity; scheduling a completed
|
|
1431
|
+
// publication creates a fresh occurrence without recapturing its media.
|
|
1424
1432
|
}
|
|
1425
1433
|
|
|
1426
1434
|
export async function evaluateContentBodyForPublish(
|
package/src/routes/content.ts
CHANGED
|
@@ -389,6 +389,24 @@ const ContentMetadataSchema = z
|
|
|
389
389
|
})
|
|
390
390
|
.transform((metadata) => metadata as ContentMetadata)
|
|
391
391
|
|
|
392
|
+
// Known Nextcloud share pages are HTML, never ingestable media. The editor
|
|
393
|
+
// must save the direct-download URL so the existing provider validates bytes.
|
|
394
|
+
const MediaSourceUrlSchema = z
|
|
395
|
+
.string()
|
|
396
|
+
.url()
|
|
397
|
+
.refine(
|
|
398
|
+
(value) => {
|
|
399
|
+
let url: URL
|
|
400
|
+
try {
|
|
401
|
+
url = new URL(value)
|
|
402
|
+
} catch {
|
|
403
|
+
return false
|
|
404
|
+
}
|
|
405
|
+
return !(url.hostname === 'drive.fulcrum-portal.com' && /^\/s\/[^/]+\/?$/.test(url.pathname))
|
|
406
|
+
},
|
|
407
|
+
{ message: 'Use the Nextcloud direct-download URL ending in /download, not its share page.' },
|
|
408
|
+
)
|
|
409
|
+
|
|
392
410
|
function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
393
411
|
const category = slugEnum(resolved.primaryCategorySlugs)
|
|
394
412
|
const topic = slugEnum(resolved.primaryTopicSlugs)
|
|
@@ -430,7 +448,7 @@ function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
|
430
448
|
videoId: z.string().min(1),
|
|
431
449
|
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
432
450
|
thumbnailImageId: z.string().optional().nullable(),
|
|
433
|
-
processingSourceUrl:
|
|
451
|
+
processingSourceUrl: MediaSourceUrlSchema.optional().nullable(),
|
|
434
452
|
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
435
453
|
}),
|
|
436
454
|
}),
|
|
@@ -450,7 +468,7 @@ function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
|
450
468
|
// there because a video always has a slug-derived identity up front.
|
|
451
469
|
audioR2Key: z.string().default(''),
|
|
452
470
|
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
453
|
-
processingSourceUrl:
|
|
471
|
+
processingSourceUrl: MediaSourceUrlSchema.optional().nullable(),
|
|
454
472
|
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
455
473
|
}),
|
|
456
474
|
}),
|
|
@@ -515,7 +533,7 @@ const VideoContentUpdateSchema = z.object({
|
|
|
515
533
|
videoId: z.string().optional(),
|
|
516
534
|
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
517
535
|
thumbnailImageId: z.string().optional().nullable(),
|
|
518
|
-
processingSourceUrl:
|
|
536
|
+
processingSourceUrl: MediaSourceUrlSchema.optional().nullable(),
|
|
519
537
|
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
520
538
|
})
|
|
521
539
|
|
|
@@ -523,7 +541,7 @@ const PodcastContentUpdateSchema = z.object({
|
|
|
523
541
|
transcript: z.string().optional(),
|
|
524
542
|
audioR2Key: z.string().optional(),
|
|
525
543
|
durationSeconds: z.number().int().positive().optional().nullable(),
|
|
526
|
-
processingSourceUrl:
|
|
544
|
+
processingSourceUrl: MediaSourceUrlSchema.optional().nullable(),
|
|
527
545
|
processingSourceKind: z.string().min(1).optional().nullable(),
|
|
528
546
|
})
|
|
529
547
|
|
package/src/schema/migrations.ts
CHANGED
|
@@ -902,6 +902,14 @@ CREATE INDEX idx_cms_foundry_articles_updated
|
|
|
902
902
|
ON cms_foundry_articles(updated_at DESC);
|
|
903
903
|
`,
|
|
904
904
|
},
|
|
905
|
+
{
|
|
906
|
+
id: '0030_media_publication_guard',
|
|
907
|
+
sql: `
|
|
908
|
+
ALTER TABLE video_content ADD COLUMN processing_publication_guard TEXT;
|
|
909
|
+
ALTER TABLE podcast_content ADD COLUMN processing_publication_guard TEXT;
|
|
910
|
+
ALTER TABLE podcast_content ADD COLUMN processing_state TEXT CHECK (processing_state IS NULL OR processing_state IN ('pending','queued','fetching','transcribing','transcoding','ready','failed'));
|
|
911
|
+
`,
|
|
912
|
+
},
|
|
905
913
|
]
|
|
906
914
|
|
|
907
915
|
/**
|
package/src/schema/types.ts
CHANGED
|
@@ -187,6 +187,8 @@ export interface ArticleContentRow {
|
|
|
187
187
|
*/
|
|
188
188
|
export interface VideoContentRow {
|
|
189
189
|
content_id: string
|
|
190
|
+
/** Capture-time published revision fence; null for historical/unpublished captures. */
|
|
191
|
+
processing_publication_guard?: string | null
|
|
190
192
|
script: string
|
|
191
193
|
video_id: string
|
|
192
194
|
duration_seconds: number | null
|
|
@@ -211,6 +213,10 @@ export interface VideoContentRow {
|
|
|
211
213
|
|
|
212
214
|
export interface PodcastContentRow {
|
|
213
215
|
content_id: string
|
|
216
|
+
/** Explicit preparation lifecycle; historical captures remain unqueued. */
|
|
217
|
+
processing_state?: VideoProcessingState | null
|
|
218
|
+
/** Capture-time published revision fence; null for historical/unpublished captures. */
|
|
219
|
+
processing_publication_guard?: string | null
|
|
214
220
|
transcript: string
|
|
215
221
|
audio_r2_key: string
|
|
216
222
|
duration_seconds: number | null
|