Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/content/CHARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ These exist so the template stays a **template**, not a half-finished product:
| Total `.ts` LOC under `src/` | ≤ 6,000 | Readable in a half-day sitting |
| Locales | 2 (`en`, `zh-CN`) | Proves multi-locale without becoming a translation project |
| Dashboards | 3 | Today / Calendar / ROI — each shows a distinct widget class |
| Flows | ≤ 5 | Event-driven only (no cron in spec v6); time-based work ships as record actions. Cap raised 4→5 (2026-06): the package already ships five distinct event-driven flows — signal→topic promotion, CTA default, publish approval, publication rollup, and lifecycle notifications (the latter was renamed from `stamp_lifecycle_timestamps` once timestamp-stamping moved into the piece hook). |
| Flows | ≤ 5 | Event-driven only (no cron in spec v6); time-based work ships as record actions. The package ships four event-driven flows — signal→topic promotion, CTA default, publish approval, and lifecycle notifications (renamed from `stamp_lifecycle_timestamps` once timestamp-stamping moved into the piece hook). A former fifth, `publication_rollup`, was retired once the metric→publication→piece totals became native `Field.summary` roll-ups (its script-node "aggregations" never ran — the automation engine has no aggregate node). |
| State machines | 2 | piece + signal |
| Approval processes | 1 | publish gate |
| Sharing rules | 1 | topic-scoped |
Expand Down
12 changes: 8 additions & 4 deletions packages/content/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { pieceHook, signalHook } from '../objects/content_piece.hook';

// NOTE: the metric → publication → piece totals (`total_views` etc.) are native
// `Field.summary` roll-ups on content_publication / content_piece (#1870) — the
// engine recomputes them server-side on child insert/update/delete. They are NOT
// hook-maintained: a cross-object rollup write from a hook is unsupported in the
// standalone QuickJS runtime (nested writes crash; `ctx.services.data` is
// undefined inside it), which is why the declarative summary-field route is used.
// engine recomputes them server-side on child insert/update/delete. A pure
// sum/max like these is expressed declaratively as a `summary` field rather than
// a hook because it needs to be delete-safe: the engine captures the child's FK
// pre-image on delete and recomputes the parent, whereas an `afterDelete` hook
// only receives `{ id }` and can't tell which parent to fix. (Nested cross-object
// writes from hooks are now safe — framework#1867 is fixed — so a hook is the
// right tool for a *non-aggregate* rollup; for these pure aggregates the summary
// field stays the better fit.)
export const allHooks = [pieceHook, signalHook];
12 changes: 7 additions & 5 deletions packages/content/src/objects/content_publication.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
* them server-side from the child `content_metric` rows, and they cascade one
* level up to the parent `content_piece`'s own `total_*` summaries. (A piece
* with no publications yet has no child to trigger a recompute, so its rollups
* read `null` until its first publication exists.) A hook can't maintain these —
* a nested cross-object write is unsupported in the standalone runtime; see
* read `null` until its first publication exists.) These are declared as
* `summary` fields rather than hook-maintained because a pure aggregate needs a
* delete-safe recompute — which the engine does natively off the child FK; see
* content/src/hooks/index.ts.
*/
export const Publication = ObjectSchema.create({
Expand Down Expand Up @@ -67,9 +68,10 @@ export const Publication = ObjectSchema.create({
// recomputes these server-side whenever a child `content_metric` row is
// inserted/updated/deleted (FK `content_metric.publication` auto-detected).
// This replaces the old `publication_rollup` flow, whose script-node
// "aggregations" never ran (the automation engine has no aggregate node) —
// and a hook can't do it either (nested cross-object write is unsupported in
// the standalone QuickJS runtime; see content/src/hooks/index.ts).
// "aggregations" never ran (the automation engine has no aggregate node). A
// hook could do it now (nested cross-object writes are safe since
// framework#1867), but a `summary` field is the delete-safe, declarative fit
// for a pure aggregate; see content/src/hooks/index.ts.
total_views: Field.summary({
label: 'Views',
group: 'rollups',
Expand Down
Loading