@byline/core 4.14.1 → 4.16.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.
Files changed (62) hide show
  1. package/dist/@types/db-types.d.ts +222 -0
  2. package/dist/@types/site-config.d.ts +24 -0
  3. package/dist/codegen/index.test.node.js +1 -1
  4. package/dist/core.d.ts +8 -0
  5. package/dist/core.js +39 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +8 -11
  8. package/dist/scheduler/define-recurring-task.d.ts +19 -0
  9. package/dist/scheduler/define-recurring-task.js +20 -0
  10. package/dist/scheduler/index.d.ts +19 -0
  11. package/dist/scheduler/index.js +18 -0
  12. package/dist/scheduler/run-due-tasks.d.ts +39 -0
  13. package/dist/scheduler/run-due-tasks.js +324 -0
  14. package/dist/scheduler/run-due-tasks.test.node.d.ts +8 -0
  15. package/dist/scheduler/run-due-tasks.test.node.js +396 -0
  16. package/dist/scheduler/scheduled-publication-constants.d.ts +11 -0
  17. package/dist/scheduler/scheduled-publication-constants.js +11 -0
  18. package/dist/scheduler/scheduled-publication.d.ts +31 -0
  19. package/dist/scheduler/scheduled-publication.js +198 -0
  20. package/dist/scheduler/scheduled-publication.test.node.d.ts +8 -0
  21. package/dist/scheduler/scheduled-publication.test.node.js +109 -0
  22. package/dist/scheduler/scheduler-boot.test.node.d.ts +8 -0
  23. package/dist/scheduler/scheduler-boot.test.node.js +103 -0
  24. package/dist/scheduler/ticker.d.ts +34 -0
  25. package/dist/scheduler/ticker.js +144 -0
  26. package/dist/scheduler/ticker.test.node.d.ts +8 -0
  27. package/dist/scheduler/ticker.test.node.js +249 -0
  28. package/dist/scheduler/types.d.ts +241 -0
  29. package/dist/scheduler/types.js +8 -0
  30. package/dist/scheduler/validate-scheduler-config.d.ts +19 -0
  31. package/dist/scheduler/validate-scheduler-config.js +25 -0
  32. package/dist/scheduler/validate-scheduler-config.test.node.d.ts +8 -0
  33. package/dist/scheduler/validate-scheduler-config.test.node.js +38 -0
  34. package/dist/scheduler/validate-tasks.d.ts +14 -0
  35. package/dist/scheduler/validate-tasks.js +40 -0
  36. package/dist/scheduler/validate-tasks.test.node.d.ts +8 -0
  37. package/dist/scheduler/validate-tasks.test.node.js +49 -0
  38. package/dist/services/collection-bootstrap.test.node.js +2 -0
  39. package/dist/services/discover-counter-groups.test.node.js +2 -0
  40. package/dist/services/document-lifecycle/audit.d.ts +6 -0
  41. package/dist/services/document-lifecycle/audit.js +6 -0
  42. package/dist/services/document-lifecycle/copy-to-locale.js +15 -10
  43. package/dist/services/document-lifecycle/delete-locale.js +18 -10
  44. package/dist/services/document-lifecycle/delete.js +8 -0
  45. package/dist/services/document-lifecycle/index.d.ts +1 -0
  46. package/dist/services/document-lifecycle/index.js +1 -0
  47. package/dist/services/document-lifecycle/publish-schedule-consistency.d.ts +36 -0
  48. package/dist/services/document-lifecycle/publish-schedule-consistency.js +80 -0
  49. package/dist/services/document-lifecycle/restore.js +15 -10
  50. package/dist/services/document-lifecycle/scheduled-publish.d.ts +51 -0
  51. package/dist/services/document-lifecycle/scheduled-publish.js +351 -0
  52. package/dist/services/document-lifecycle/status-transition.d.ts +42 -0
  53. package/dist/services/document-lifecycle/status-transition.js +41 -0
  54. package/dist/services/document-lifecycle/status-transition.test.node.d.ts +8 -0
  55. package/dist/services/document-lifecycle/status-transition.test.node.js +145 -0
  56. package/dist/services/document-lifecycle/status.js +30 -23
  57. package/dist/services/document-lifecycle/tree.test.node.js +3 -0
  58. package/dist/services/document-lifecycle/update.js +35 -30
  59. package/dist/services/document-lifecycle.test.node.js +262 -2
  60. package/dist/services/field-upload.test.node.js +2 -0
  61. package/dist/services/populate.test.node.js +2 -0
  62. package/package.json +7 -2
@@ -0,0 +1,36 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { DocumentPublishSchedule } from '../../@types/index.js';
9
+ import type { AuditCapability } from './audit.js';
10
+ import type { DocumentLifecycleContext } from './context.js';
11
+ export type PublishScheduleInvalidationReason = 'content_edited' | 'status_changed' | 'unpublished' | 'soft_deleted';
12
+ /** Bounded, JSON-safe audit projection; execution errors never enter audit. */
13
+ export declare function publishScheduleAuditValue(schedule: DocumentPublishSchedule): Record<string, unknown>;
14
+ /**
15
+ * Lock and remove a schedule inside an already-open lifecycle transaction.
16
+ * The caller appends the audit after its primary mutation audit so ordering is
17
+ * deterministic. This helper deliberately never opens a transaction.
18
+ */
19
+ export declare function cancelPublishScheduleInTransaction(ctx: DocumentLifecycleContext, documentId: string): Promise<DocumentPublishSchedule | null>;
20
+ /** Append the audit for a cancellation already performed in this transaction. */
21
+ export declare function appendPublishScheduleCancellationAudit(params: {
22
+ ctx: DocumentLifecycleContext;
23
+ audit: AuditCapability;
24
+ schedule: DocumentPublishSchedule | null;
25
+ reason: Exclude<PublishScheduleInvalidationReason, 'content_edited'>;
26
+ }): Promise<void>;
27
+ /**
28
+ * Commit a new content version and schedule suspension in one ambient
29
+ * transaction. `write` may use the adapter's nested transaction/savepoint,
30
+ * but every rejection propagates to this outer boundary.
31
+ */
32
+ export declare function commitContentVersionWithScheduleSuspension<T>(params: {
33
+ ctx: DocumentLifecycleContext;
34
+ documentId: string;
35
+ write: () => Promise<T>;
36
+ }): Promise<T>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { AUDIT_ACTIONS, auditActor, requireAuditCapability } from './audit.js';
9
+ /** Bounded, JSON-safe audit projection; execution errors never enter audit. */
10
+ export function publishScheduleAuditValue(schedule) {
11
+ return {
12
+ state: schedule.state,
13
+ publishAt: schedule.publishAt.toISOString(),
14
+ targetVersionId: schedule.targetVersionId,
15
+ lastAuthorizedBy: schedule.lastAuthorizedBy,
16
+ };
17
+ }
18
+ /**
19
+ * Lock and remove a schedule inside an already-open lifecycle transaction.
20
+ * The caller appends the audit after its primary mutation audit so ordering is
21
+ * deterministic. This helper deliberately never opens a transaction.
22
+ */
23
+ export function cancelPublishScheduleInTransaction(ctx, documentId) {
24
+ return ctx.db.commands.documents.publishSchedules.cancel({
25
+ documentId,
26
+ collectionId: ctx.collectionId,
27
+ });
28
+ }
29
+ /** Append the audit for a cancellation already performed in this transaction. */
30
+ export async function appendPublishScheduleCancellationAudit(params) {
31
+ if (params.schedule === null)
32
+ return;
33
+ const actor = auditActor(params.ctx);
34
+ await params.audit.append({
35
+ documentId: params.schedule.documentId,
36
+ collectionId: params.schedule.collectionId,
37
+ actorId: actor.actorId,
38
+ actorRealm: actor.actorRealm,
39
+ action: AUDIT_ACTIONS.publishScheduleCancelled,
40
+ field: 'scheduled_publish',
41
+ before: publishScheduleAuditValue(params.schedule),
42
+ after: { reason: params.reason },
43
+ });
44
+ }
45
+ /**
46
+ * Commit a new content version and schedule suspension in one ambient
47
+ * transaction. `write` may use the adapter's nested transaction/savepoint,
48
+ * but every rejection propagates to this outer boundary.
49
+ */
50
+ export async function commitContentVersionWithScheduleSuspension(params) {
51
+ const audit = requireAuditCapability(params.ctx.db);
52
+ const actor = auditActor(params.ctx);
53
+ return audit.withTransaction(async () => {
54
+ const result = await params.write();
55
+ const suspension = await params.ctx.db.commands.documents.publishSchedules.suspendForContentEdit({
56
+ documentId: params.documentId,
57
+ collectionId: params.ctx.collectionId,
58
+ });
59
+ if (suspension.status === 'suspended') {
60
+ await audit.append({
61
+ documentId: params.documentId,
62
+ collectionId: params.ctx.collectionId,
63
+ actorId: actor.actorId,
64
+ actorRealm: actor.actorRealm,
65
+ action: AUDIT_ACTIONS.publishScheduleSuspended,
66
+ field: 'scheduled_publish',
67
+ before: {
68
+ state: 'armed',
69
+ targetVersionId: suspension.schedule.targetVersionId,
70
+ publishAt: suspension.schedule.publishAt.toISOString(),
71
+ },
72
+ after: {
73
+ state: 'needs_reconfirm',
74
+ reason: 'content_edited',
75
+ },
76
+ });
77
+ }
78
+ return result;
79
+ });
80
+ }
@@ -11,6 +11,7 @@ import { ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib
11
11
  import { withLogContext } from '../../lib/logger.js';
12
12
  import { getDefaultStatus } from '../../workflow/workflow.js';
13
13
  import { actorId, applyRichTextEmbed, extractDocumentId, extractVersionId, invokeHook, } from './internals.js';
14
+ import { commitContentVersionWithScheduleSuspension } from './publish-schedule-consistency.js';
14
15
  /**
15
16
  * Restore a historical document version as the new current version.
16
17
  *
@@ -129,17 +130,21 @@ export async function restoreDocumentVersion(ctx, params) {
129
130
  //
130
131
  // No `path` is passed: restore does not change the document's path
131
132
  // (the existing byline_document_paths row stays as-is — sticky).
132
- const result = await db.commands.documents.createDocumentVersion({
133
+ const result = await commitContentVersionWithScheduleSuspension({
134
+ ctx,
133
135
  documentId: params.documentId,
134
- collectionId,
135
- collectionVersion: ctx.collectionVersion,
136
- collectionConfig: definition,
137
- action: 'restore',
138
- documentData: sourceFields,
139
- status: getDefaultStatus(definition),
140
- locale: 'all',
141
- previousVersionId: currentMeta.document_version_id,
142
- createdBy: actorId(ctx),
136
+ write: () => db.commands.documents.createDocumentVersion({
137
+ documentId: params.documentId,
138
+ collectionId,
139
+ collectionVersion: ctx.collectionVersion,
140
+ collectionConfig: definition,
141
+ action: 'restore',
142
+ documentData: sourceFields,
143
+ status: getDefaultStatus(definition),
144
+ locale: 'all',
145
+ previousVersionId: currentMeta.document_version_id,
146
+ createdBy: actorId(ctx),
147
+ }),
143
148
  });
144
149
  const documentId = extractDocumentId(result.document) || params.documentId;
145
150
  const documentVersionId = extractVersionId(result.document);
@@ -0,0 +1,51 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { type RequestContext } from '@byline/auth';
9
+ import type { DocumentPublishSchedule, DocumentPublishSchedulePage, DocumentPublishScheduleState } from '../../@types/index.js';
10
+ import type { BylineCore } from '../../core.js';
11
+ import type { DocumentLifecycleContext } from './context.js';
12
+ export type ClaimedScheduledPublicationResult = {
13
+ status: 'published';
14
+ } | {
15
+ status: 'claim_lost';
16
+ } | {
17
+ status: 'target_changed';
18
+ } | {
19
+ status: 'terminal';
20
+ reason: 'document_not_found' | 'already_published' | 'invalid_transition';
21
+ };
22
+ export declare function scheduleDocumentPublish(ctx: DocumentLifecycleContext, params: {
23
+ documentId: string;
24
+ publishAt: string;
25
+ expectedVersionId: string;
26
+ }): Promise<DocumentPublishSchedule>;
27
+ export declare function confirmDocumentScheduledPublish(ctx: DocumentLifecycleContext, params: {
28
+ documentId: string;
29
+ expectedVersionId: string;
30
+ }): Promise<DocumentPublishSchedule>;
31
+ export declare function cancelDocumentScheduledPublish(ctx: DocumentLifecycleContext, params: {
32
+ documentId: string;
33
+ }): Promise<DocumentPublishSchedule | null>;
34
+ export declare function getDocumentScheduledPublish(ctx: DocumentLifecycleContext, params: {
35
+ documentId: string;
36
+ }): Promise<DocumentPublishSchedule | null>;
37
+ export declare function listDocumentPublishSchedules(core: BylineCore, requestContext: RequestContext | undefined, params: {
38
+ states?: readonly DocumentPublishScheduleState[];
39
+ lastAuthorizedBy?: string;
40
+ page: number;
41
+ pageSize: number;
42
+ }): Promise<DocumentPublishSchedulePage>;
43
+ /**
44
+ * Execute one token-fenced publication through the normal status lifecycle.
45
+ * Internal to the server-only sweep: authorization was captured when the row
46
+ * was armed, so callers must construct an explicit system context.
47
+ */
48
+ export declare function publishClaimedScheduledDocument(ctx: DocumentLifecycleContext, params: {
49
+ documentId: string;
50
+ executionToken: string;
51
+ }): Promise<ClaimedScheduledPublicationResult>;
@@ -0,0 +1,351 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { ERR_UNAUTHENTICATED } from '@byline/auth';
9
+ import { resolveHooks } from '../../@types/index.js';
10
+ import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
11
+ import { collectionAbilityKey } from '../../auth/register-collection-abilities.js';
12
+ import { ERR_CONFLICT, ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_VALIDATION, } from '../../lib/errors.js';
13
+ import { withLogContext } from '../../lib/logger.js';
14
+ import { getWorkflow, validateStatusTransition } from '../../workflow/workflow.js';
15
+ import { AUDIT_ACTIONS, auditActor, requireAuditCapability } from './audit.js';
16
+ import { actorId, invokeHook } from './internals.js';
17
+ import { publishScheduleAuditValue } from './publish-schedule-consistency.js';
18
+ import { commitDocumentStatusTransition } from './status-transition.js';
19
+ class ClaimedPublicationOutcome extends Error {
20
+ result;
21
+ constructor(result) {
22
+ super(`scheduled publication did not commit: ${result.status}`);
23
+ this.result = result;
24
+ }
25
+ }
26
+ function assertScheduleAbilities(ctx) {
27
+ assertActorCanPerform(ctx.requestContext, ctx.collectionPath, 'changeStatus');
28
+ assertActorCanPerform(ctx.requestContext, ctx.collectionPath, 'publish');
29
+ }
30
+ async function assertCurrentVersionCanPublish(ctx, documentId, expectedVersionId) {
31
+ const current = await ctx.db.queries.documents.getCurrentVersionMetadata({
32
+ collection_id: ctx.collectionId,
33
+ document_id: documentId,
34
+ });
35
+ if (current === null) {
36
+ throw ERR_NOT_FOUND({
37
+ message: 'document not found',
38
+ details: { documentId },
39
+ }).log(ctx.logger);
40
+ }
41
+ if (current.document_version_id !== expectedVersionId) {
42
+ throw ERR_CONFLICT({
43
+ message: 'the document changed before its publication schedule could be saved',
44
+ details: {
45
+ documentId,
46
+ expectedVersionId,
47
+ currentVersionId: current.document_version_id,
48
+ },
49
+ }).log(ctx.logger);
50
+ }
51
+ const workflow = getWorkflow(ctx.definition);
52
+ if (workflow.statuses.length <= 1 || current.status === 'published') {
53
+ throw ERR_INVALID_TRANSITION({
54
+ message: `document in status '${current.status}' cannot be scheduled for publication`,
55
+ details: { documentId, currentStatus: current.status },
56
+ }).log(ctx.logger);
57
+ }
58
+ const transition = validateStatusTransition(workflow, current.status, 'published');
59
+ if (!transition.valid) {
60
+ throw ERR_INVALID_TRANSITION({
61
+ message: transition.reason ?? `invalid status transition from '${current.status}' to 'published'`,
62
+ details: { documentId, currentStatus: current.status, nextStatus: 'published' },
63
+ }).log(ctx.logger);
64
+ }
65
+ }
66
+ function parsePublishAt(value) {
67
+ const isoInstant = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
68
+ const parsed = new Date(value);
69
+ if (!isoInstant.test(value) || Number.isNaN(parsed.getTime())) {
70
+ throw ERR_VALIDATION({
71
+ message: 'publishAt must be a valid ISO instant',
72
+ details: { publishAt: value },
73
+ });
74
+ }
75
+ return parsed;
76
+ }
77
+ function throwScheduleStorageOutcome(ctx, documentId, status) {
78
+ if (status === 'document_not_found') {
79
+ throw ERR_NOT_FOUND({ message: 'document not found', details: { documentId } }).log(ctx.logger);
80
+ }
81
+ if (status === 'publish_at_not_future') {
82
+ throw ERR_VALIDATION({
83
+ message: 'scheduled publication must be in the future according to database time',
84
+ details: { documentId },
85
+ }).log(ctx.logger);
86
+ }
87
+ throw ERR_CONFLICT({
88
+ message: status === 'execution_in_progress'
89
+ ? 'scheduled publication is already being finalized; refresh before rescheduling'
90
+ : 'the document changed before its publication schedule could be saved',
91
+ details: { documentId, status },
92
+ }).log(ctx.logger);
93
+ }
94
+ export async function scheduleDocumentPublish(ctx, params) {
95
+ return withLogContext({ domain: 'services', module: 'lifecycle', function: 'scheduleDocumentPublish' }, async () => {
96
+ assertScheduleAbilities(ctx);
97
+ await assertCurrentVersionCanPublish(ctx, params.documentId, params.expectedVersionId);
98
+ const publishAt = parsePublishAt(params.publishAt);
99
+ const audit = requireAuditCapability(ctx.db);
100
+ const actor = auditActor(ctx);
101
+ return audit.withTransaction(async () => {
102
+ const result = await ctx.db.commands.documents.publishSchedules.schedule({
103
+ documentId: params.documentId,
104
+ collectionId: ctx.collectionId,
105
+ expectedVersionId: params.expectedVersionId,
106
+ publishAt,
107
+ actorId: actorId(ctx) ?? null,
108
+ });
109
+ if (result.status !== 'scheduled') {
110
+ return throwScheduleStorageOutcome(ctx, params.documentId, result.status);
111
+ }
112
+ await audit.append({
113
+ documentId: params.documentId,
114
+ collectionId: ctx.collectionId,
115
+ actorId: actor.actorId,
116
+ actorRealm: actor.actorRealm,
117
+ action: result.previous === null
118
+ ? AUDIT_ACTIONS.publishScheduled
119
+ : AUDIT_ACTIONS.publishRescheduled,
120
+ field: 'scheduled_publish',
121
+ before: result.previous === null ? null : publishScheduleAuditValue(result.previous),
122
+ after: publishScheduleAuditValue(result.schedule),
123
+ });
124
+ return result.schedule;
125
+ });
126
+ });
127
+ }
128
+ export async function confirmDocumentScheduledPublish(ctx, params) {
129
+ return withLogContext({ domain: 'services', module: 'lifecycle', function: 'confirmDocumentScheduledPublish' }, async () => {
130
+ assertScheduleAbilities(ctx);
131
+ await assertCurrentVersionCanPublish(ctx, params.documentId, params.expectedVersionId);
132
+ const audit = requireAuditCapability(ctx.db);
133
+ const actor = auditActor(ctx);
134
+ return audit.withTransaction(async () => {
135
+ const result = await ctx.db.commands.documents.publishSchedules.confirm({
136
+ documentId: params.documentId,
137
+ collectionId: ctx.collectionId,
138
+ expectedVersionId: params.expectedVersionId,
139
+ actorId: actorId(ctx) ?? null,
140
+ });
141
+ if (result.status !== 'confirmed') {
142
+ if (result.status === 'schedule_not_found') {
143
+ throw ERR_NOT_FOUND({
144
+ message: 'scheduled publication not found',
145
+ details: { documentId: params.documentId },
146
+ }).log(ctx.logger);
147
+ }
148
+ throw ERR_CONFLICT({
149
+ message: result.status === 'not_suspended'
150
+ ? 'scheduled publication does not require re-confirmation'
151
+ : 'the document changed before its publication could be re-confirmed',
152
+ details: { documentId: params.documentId, status: result.status },
153
+ }).log(ctx.logger);
154
+ }
155
+ await audit.append({
156
+ documentId: params.documentId,
157
+ collectionId: ctx.collectionId,
158
+ actorId: actor.actorId,
159
+ actorRealm: actor.actorRealm,
160
+ action: AUDIT_ACTIONS.publishReconfirmed,
161
+ field: 'scheduled_publish',
162
+ before: {
163
+ state: 'needs_reconfirm',
164
+ targetVersionId: result.previousTargetVersionId,
165
+ },
166
+ after: publishScheduleAuditValue(result.schedule),
167
+ });
168
+ return result.schedule;
169
+ });
170
+ });
171
+ }
172
+ export async function cancelDocumentScheduledPublish(ctx, params) {
173
+ return withLogContext({ domain: 'services', module: 'lifecycle', function: 'cancelDocumentScheduledPublish' }, async () => {
174
+ assertScheduleAbilities(ctx);
175
+ const audit = requireAuditCapability(ctx.db);
176
+ const actor = auditActor(ctx);
177
+ return audit.withTransaction(async () => {
178
+ const schedule = await ctx.db.commands.documents.publishSchedules.cancel({
179
+ documentId: params.documentId,
180
+ collectionId: ctx.collectionId,
181
+ });
182
+ if (schedule !== null) {
183
+ await audit.append({
184
+ documentId: params.documentId,
185
+ collectionId: ctx.collectionId,
186
+ actorId: actor.actorId,
187
+ actorRealm: actor.actorRealm,
188
+ action: AUDIT_ACTIONS.publishScheduleCancelled,
189
+ field: 'scheduled_publish',
190
+ before: publishScheduleAuditValue(schedule),
191
+ after: { reason: 'explicit' },
192
+ });
193
+ }
194
+ return schedule;
195
+ });
196
+ });
197
+ }
198
+ export async function getDocumentScheduledPublish(ctx, params) {
199
+ assertScheduleAbilities(ctx);
200
+ return ctx.db.queries.documents.publishSchedules.get({
201
+ documentId: params.documentId,
202
+ collectionId: ctx.collectionId,
203
+ });
204
+ }
205
+ export async function listDocumentPublishSchedules(core, requestContext, params) {
206
+ const actor = requestContext?.actor;
207
+ if (actor == null) {
208
+ throw ERR_UNAUTHENTICATED({
209
+ message: 'listing scheduled publications requires an authenticated actor',
210
+ });
211
+ }
212
+ if (!Number.isInteger(params.page) || params.page < 1) {
213
+ throw ERR_VALIDATION({ message: 'page must be a positive integer' });
214
+ }
215
+ if (!Number.isInteger(params.pageSize) || params.pageSize < 1 || params.pageSize > 100) {
216
+ throw ERR_VALIDATION({ message: 'pageSize must be an integer between 1 and 100' });
217
+ }
218
+ const collectionIds = [];
219
+ for (const definition of core.collections) {
220
+ if (actor.hasAbility(collectionAbilityKey(definition.path, 'changeStatus')) &&
221
+ actor.hasAbility(collectionAbilityKey(definition.path, 'publish'))) {
222
+ collectionIds.push(core.getCollectionRecord(definition.path).collectionId);
223
+ }
224
+ }
225
+ return core.db.queries.documents.publishSchedules.list({
226
+ collectionIds,
227
+ states: params.states,
228
+ lastAuthorizedBy: params.lastAuthorizedBy,
229
+ page: params.page,
230
+ pageSize: params.pageSize,
231
+ });
232
+ }
233
+ /**
234
+ * Execute one token-fenced publication through the normal status lifecycle.
235
+ * Internal to the server-only sweep: authorization was captured when the row
236
+ * was armed, so callers must construct an explicit system context.
237
+ */
238
+ export async function publishClaimedScheduledDocument(ctx, params) {
239
+ const workflow = getWorkflow(ctx.definition);
240
+ const initial = await ctx.db.queries.documents.getCurrentVersionMetadata({
241
+ collection_id: ctx.collectionId,
242
+ document_id: params.documentId,
243
+ });
244
+ if (initial === null) {
245
+ return { status: 'terminal', reason: 'document_not_found' };
246
+ }
247
+ const schedule = await ctx.db.queries.documents.publishSchedules.get({
248
+ documentId: params.documentId,
249
+ collectionId: ctx.collectionId,
250
+ });
251
+ if (schedule === null || schedule.executionToken !== params.executionToken) {
252
+ return { status: 'claim_lost' };
253
+ }
254
+ if (initial.document_version_id !== schedule.targetVersionId) {
255
+ return { status: 'target_changed' };
256
+ }
257
+ if (initial.status === 'published') {
258
+ return { status: 'terminal', reason: 'already_published' };
259
+ }
260
+ const initialTransition = validateStatusTransition(workflow, initial.status, 'published');
261
+ if (!initialTransition.valid) {
262
+ return { status: 'terminal', reason: 'invalid_transition' };
263
+ }
264
+ const hooks = await resolveHooks(ctx.definition);
265
+ const path = (await ctx.db.queries.documents.getCurrentPath({
266
+ collection_id: ctx.collectionId,
267
+ document_id: params.documentId,
268
+ })) ?? '';
269
+ const hookCtx = {
270
+ documentId: params.documentId,
271
+ documentVersionId: initial.document_version_id,
272
+ collectionPath: ctx.collectionPath,
273
+ path,
274
+ previousStatus: initial.status,
275
+ nextStatus: 'published',
276
+ };
277
+ await invokeHook(hooks?.beforeStatusChange, hookCtx);
278
+ try {
279
+ await commitDocumentStatusTransition({
280
+ db: ctx.db,
281
+ documentId: params.documentId,
282
+ documentVersionId: initial.document_version_id,
283
+ collectionId: ctx.collectionId,
284
+ previousStatus: initial.status,
285
+ nextStatus: 'published',
286
+ actor: auditActor(ctx),
287
+ contributions: {
288
+ beforeStatusWrite: async () => {
289
+ const locked = await ctx.db.commands.documents.publishSchedules.lockClaim({
290
+ documentId: params.documentId,
291
+ executionToken: params.executionToken,
292
+ });
293
+ if (locked === null) {
294
+ throw new ClaimedPublicationOutcome({ status: 'claim_lost' });
295
+ }
296
+ const current = await ctx.db.queries.documents.getCurrentVersionMetadata({
297
+ collection_id: ctx.collectionId,
298
+ document_id: params.documentId,
299
+ });
300
+ if (current === null) {
301
+ throw new ClaimedPublicationOutcome({
302
+ status: 'terminal',
303
+ reason: 'document_not_found',
304
+ });
305
+ }
306
+ if (current.document_version_id !== locked.targetVersionId ||
307
+ current.document_version_id !== initial.document_version_id) {
308
+ throw new ClaimedPublicationOutcome({ status: 'target_changed' });
309
+ }
310
+ if (current.status === 'published') {
311
+ throw new ClaimedPublicationOutcome({
312
+ status: 'terminal',
313
+ reason: 'already_published',
314
+ });
315
+ }
316
+ if (!validateStatusTransition(workflow, current.status, 'published').valid) {
317
+ throw new ClaimedPublicationOutcome({
318
+ status: 'terminal',
319
+ reason: 'invalid_transition',
320
+ });
321
+ }
322
+ },
323
+ afterAuditAppend: async () => {
324
+ const deleted = await ctx.db.commands.documents.publishSchedules.deleteClaim({
325
+ documentId: params.documentId,
326
+ executionToken: params.executionToken,
327
+ });
328
+ if (!deleted)
329
+ throw new ClaimedPublicationOutcome({ status: 'claim_lost' });
330
+ },
331
+ },
332
+ });
333
+ }
334
+ catch (error) {
335
+ if (error instanceof ClaimedPublicationOutcome)
336
+ return error.result;
337
+ throw error;
338
+ }
339
+ try {
340
+ await invokeHook(hooks?.afterStatusChange, hookCtx);
341
+ }
342
+ catch (error) {
343
+ try {
344
+ ctx.logger.error({ err: error, documentId: params.documentId }, 'afterStatusChange hook failed after scheduled publication committed');
345
+ }
346
+ catch {
347
+ // Diagnostic logging cannot change the committed publication outcome.
348
+ }
349
+ }
350
+ return { status: 'published' };
351
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AuditActorRealm, IDbAdapter } from '../../@types/index.js';
9
+ /**
10
+ * Internal transaction contributions for the two status-transition callers.
11
+ * They run on the adapter's ambient transaction and must not open or swallow
12
+ * failures from a nested transaction. This module is deliberately absent from
13
+ * the public lifecycle barrel: these callbacks are not collection hooks or a
14
+ * supported extension API.
15
+ */
16
+ interface StatusTransitionContributions {
17
+ /** Runs before any status mutation and may abort the whole transaction. */
18
+ beforeStatusWrite?: () => void | Promise<void>;
19
+ /** Runs after the audit append, before the whole transaction commits. */
20
+ afterAuditAppend?: () => void | Promise<void>;
21
+ }
22
+ interface CommitDocumentStatusTransitionParams {
23
+ db: IDbAdapter;
24
+ documentId: string;
25
+ documentVersionId: string;
26
+ collectionId: string;
27
+ previousStatus: string;
28
+ nextStatus: string;
29
+ actor: {
30
+ actorId: string | undefined;
31
+ actorRealm: AuditActorRealm;
32
+ };
33
+ contributions?: StatusTransitionContributions;
34
+ }
35
+ /**
36
+ * Commit the shared, transaction-internal portion of a document status
37
+ * transition. Authorization, workflow validation, and lifecycle hooks remain
38
+ * the responsibility of the caller; status mutation, auto-archive, audit, and
39
+ * any caller contribution commit or roll back as one ambient transaction.
40
+ */
41
+ export declare function commitDocumentStatusTransition(params: CommitDocumentStatusTransitionParams): Promise<void>;
42
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { AUDIT_ACTIONS, requireAuditCapability } from './audit.js';
9
+ /**
10
+ * Commit the shared, transaction-internal portion of a document status
11
+ * transition. Authorization, workflow validation, and lifecycle hooks remain
12
+ * the responsibility of the caller; status mutation, auto-archive, audit, and
13
+ * any caller contribution commit or roll back as one ambient transaction.
14
+ */
15
+ export async function commitDocumentStatusTransition(params) {
16
+ const audit = requireAuditCapability(params.db);
17
+ await audit.withTransaction(async () => {
18
+ await params.contributions?.beforeStatusWrite?.();
19
+ await params.db.commands.documents.setDocumentStatus({
20
+ document_version_id: params.documentVersionId,
21
+ status: params.nextStatus,
22
+ });
23
+ if (params.nextStatus === 'published') {
24
+ await params.db.commands.documents.archivePublishedVersions({
25
+ document_id: params.documentId,
26
+ excludeVersionId: params.documentVersionId,
27
+ });
28
+ }
29
+ await audit.append({
30
+ documentId: params.documentId,
31
+ collectionId: params.collectionId,
32
+ actorId: params.actor.actorId,
33
+ actorRealm: params.actor.actorRealm,
34
+ action: AUDIT_ACTIONS.statusChanged,
35
+ field: 'status',
36
+ before: params.previousStatus,
37
+ after: params.nextStatus,
38
+ });
39
+ await params.contributions?.afterAuditAppend?.();
40
+ });
41
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};