@objectstack/trigger-schedule 15.1.0 → 16.0.0-rc.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/README.md +54 -0
- package/dist/index.d.mts +133 -1
- package/dist/index.d.ts +133 -1
- package/dist/index.js +242 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +242 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -66,3 +66,57 @@ rather than failing startup.
|
|
|
66
66
|
|
|
67
67
|
A flow that throws during a scheduled run is logged and swallowed — it never
|
|
68
68
|
crashes the job runner.
|
|
69
|
+
|
|
70
|
+
## Time-relative trigger (`TimeRelativeTriggerPlugin`)
|
|
71
|
+
|
|
72
|
+
The **declarative** answer to "act on records whose date field is coming up (or
|
|
73
|
+
overdue)" (#1874) — without the fragile date-equality-on-record-change pattern
|
|
74
|
+
(which only fires if a record happens to be edited on the threshold day) or a
|
|
75
|
+
hand-rolled cron + range query per flow.
|
|
76
|
+
|
|
77
|
+
A flow whose `start` node declares a `timeRelative` descriptor is swept on a
|
|
78
|
+
schedule and launched **once per matching record**:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
{
|
|
82
|
+
type: 'start',
|
|
83
|
+
config: {
|
|
84
|
+
timeRelative: {
|
|
85
|
+
object: 'contracts',
|
|
86
|
+
dateField: 'end_date',
|
|
87
|
+
offsetDays: [60, 30, 7], // T-minus reminders — fires on each threshold day
|
|
88
|
+
// — or — withinDays: 30 // "expiring soon" range (negative = overdue lookback)
|
|
89
|
+
filter: { status: 'active' }, // optional, ANDed with the date window
|
|
90
|
+
maxRecords: 1000, // optional per-sweep cap (default 1000)
|
|
91
|
+
},
|
|
92
|
+
schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; defaults to daily 08:00 UTC
|
|
93
|
+
condition: '...', // optional per-record start-condition gate
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The matched record rides on the automation context (`event: 'time_relative'`,
|
|
99
|
+
`record`, `params`), so the start-node `condition` gate and `{record.<field>}`
|
|
100
|
+
interpolation work exactly as for a record-change flow. Because the window is
|
|
101
|
+
evaluated **every day**, a threshold is never missed regardless of when the
|
|
102
|
+
record last changed.
|
|
103
|
+
|
|
104
|
+
| Mode | Semantics (day-granular, UTC, always includes today) |
|
|
105
|
+
| ------------------- | --------------------------------------------------------------------- |
|
|
106
|
+
| `withinDays: N` | `dateField ∈ [today, today + N]` (upcoming). `N < 0` = overdue lookback. |
|
|
107
|
+
| `offsetDays: [a,b]` | one single-day match per offset (`today + a`, `today + b`, …). |
|
|
108
|
+
|
|
109
|
+
It needs both the job service (sweep cadence) **and** the ObjectQL engine (the
|
|
110
|
+
date-window query); register it alongside the schedule trigger:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { ScheduleTriggerPlugin, TimeRelativeTriggerPlugin } from '@objectstack/plugin-trigger-schedule';
|
|
114
|
+
|
|
115
|
+
kernel
|
|
116
|
+
.use(new ScheduleTriggerPlugin()) // plain schedule flows
|
|
117
|
+
.use(new TimeRelativeTriggerPlugin()); // ← time-relative sweeps (needs the ObjectQL engine)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The discovery query runs as a system operation (RLS-bypassing — a background
|
|
121
|
+
sweep sees all rows), is capped at `maxRecords` per tick (logged when it
|
|
122
|
+
clamps), and isolates per-record failures so one bad row never aborts the sweep.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
2
2
|
import { AutomationContext, JobSchedule, JobHandler } from '@objectstack/spec/contracts';
|
|
3
|
+
import { TimeRelativeTrigger as TimeRelativeTrigger$1 } from '@objectstack/spec/automation';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* ScheduleTriggerPlugin
|
|
@@ -74,6 +75,14 @@ interface TriggerLogger {
|
|
|
74
75
|
info(msg: string, ...args: unknown[]): void;
|
|
75
76
|
warn(msg: string, ...args: unknown[]): void;
|
|
76
77
|
debug?(msg: string, ...args: unknown[]): void;
|
|
78
|
+
/**
|
|
79
|
+
* Execution failures log here when available (falling back to `warn`).
|
|
80
|
+
* ERROR matters operationally: the CLI's boot-quiet window swallows
|
|
81
|
+
* stdout (debug/info/warn) but stderr (error/fatal) always lands — so a
|
|
82
|
+
* per-record sweep failure stays visible. Mirrors the record-change
|
|
83
|
+
* trigger's logger surface.
|
|
84
|
+
*/
|
|
85
|
+
error?(msg: string, ...args: unknown[]): void;
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
79
88
|
* Normalize a flow's raw `schedule` descriptor into a {@link JobSchedule}, or
|
|
@@ -108,4 +117,127 @@ declare class ScheduleTrigger implements FlowTrigger {
|
|
|
108
117
|
stop(flowName: string): void;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
/**
|
|
121
|
+
* TimeRelativeTriggerPlugin
|
|
122
|
+
*
|
|
123
|
+
* Arms **declarative time-relative flows** (#1874): a flow whose start node
|
|
124
|
+
* declares `config.timeRelative` (object + dateField + `withinDays`/`offsetDays`)
|
|
125
|
+
* is swept on a schedule and launched once per record whose date field falls in
|
|
126
|
+
* the window — no hand-written cron + range query, no fragile
|
|
127
|
+
* date-equality-on-record-change.
|
|
128
|
+
*
|
|
129
|
+
* It ships in `@objectstack/trigger-schedule` alongside the plain schedule
|
|
130
|
+
* trigger (both are schedule-driven) but is a **separate** plugin: the
|
|
131
|
+
* time-relative trigger additionally needs the ObjectQL engine (for the sweep
|
|
132
|
+
* query), so keeping it separate leaves the plain `ScheduleTriggerPlugin`'s
|
|
133
|
+
* dependency surface unchanged. Depends on the job service (sweep cadence) and
|
|
134
|
+
* the ObjectQL engine (record discovery); both are resolved lazily per `start()`
|
|
135
|
+
* so adapter upgrades are always picked up.
|
|
136
|
+
*/
|
|
137
|
+
declare class TimeRelativeTriggerPlugin implements Plugin {
|
|
138
|
+
name: string;
|
|
139
|
+
type: string;
|
|
140
|
+
version: string;
|
|
141
|
+
dependencies: string[];
|
|
142
|
+
init(ctx: PluginContext): Promise<void>;
|
|
143
|
+
start(ctx: PluginContext): Promise<void>;
|
|
144
|
+
private resolveService;
|
|
145
|
+
private resolveDataEngine;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The slice of the ObjectQL data engine this trigger needs: run a filtered
|
|
150
|
+
* `find` (to discover the records whose date field falls in the window) and,
|
|
151
|
+
* optionally, probe whether an object is registered. Typed structurally — same
|
|
152
|
+
* decoupling pattern the record-change trigger uses for its hook surface — so
|
|
153
|
+
* this plugin does not take a build dependency on the engine package.
|
|
154
|
+
*/
|
|
155
|
+
interface TimeRelativeDataEngine {
|
|
156
|
+
find(objectName: string, query?: {
|
|
157
|
+
where?: Record<string, unknown>;
|
|
158
|
+
fields?: string[];
|
|
159
|
+
limit?: number;
|
|
160
|
+
/** Elevated context — a background sweep must see all rows, not RLS-scoped ones. */
|
|
161
|
+
context?: {
|
|
162
|
+
isSystem?: boolean;
|
|
163
|
+
};
|
|
164
|
+
}): Promise<Array<Record<string, unknown>> | undefined>;
|
|
165
|
+
/**
|
|
166
|
+
* Optional object-existence probe (the ObjectQL engine's `getObject`).
|
|
167
|
+
* When present, {@link TimeRelativeTrigger.start} uses it to call out a
|
|
168
|
+
* descriptor whose `object` matches no registered object at bind time —
|
|
169
|
+
* otherwise the sweep just quietly finds nothing forever.
|
|
170
|
+
*/
|
|
171
|
+
getObject?(name: string): unknown;
|
|
172
|
+
}
|
|
173
|
+
/** A closed, inclusive instant window `[gte, lte]` as ISO-8601 strings. */
|
|
174
|
+
interface DateWindow {
|
|
175
|
+
/** Lower bound (inclusive), ISO-8601. */
|
|
176
|
+
gte: string;
|
|
177
|
+
/** Upper bound (inclusive), ISO-8601. */
|
|
178
|
+
lte: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Compute the inclusive date window(s) a descriptor selects, relative to `now`.
|
|
182
|
+
*
|
|
183
|
+
* - `offsetDays` → one single-day window per offset (`today + offset`), so the
|
|
184
|
+
* sweep fires exactly on each threshold day (the robust T-minus reminder).
|
|
185
|
+
* - `withinDays` → one range window: `[today, today + N]` when N ≥ 0 (upcoming),
|
|
186
|
+
* or `[today − |N|, today]` when N < 0 (overdue lookback). Always includes today.
|
|
187
|
+
*
|
|
188
|
+
* Day-granular and computed in UTC. The upper bound is the *end* of its day
|
|
189
|
+
* (`23:59:59.999Z`), so a `datetime` field matches for the whole day and a
|
|
190
|
+
* `date` field (compared as `YYYY-MM-DD` after the driver truncates) is inclusive.
|
|
191
|
+
*/
|
|
192
|
+
declare function computeDateWindows(desc: TimeRelativeTrigger$1, now: Date): DateWindow[];
|
|
193
|
+
/**
|
|
194
|
+
* Build the ObjectQL `where` map for one date window: the descriptor's static
|
|
195
|
+
* `filter` (if any) ANDed with a `$gte`/`$lte` range on the date field. The map
|
|
196
|
+
* form is the canonical filter shape both drivers evaluate verbatim (the same
|
|
197
|
+
* shape the platform's own retention sweep uses).
|
|
198
|
+
*/
|
|
199
|
+
declare function buildWindowWhere(desc: TimeRelativeTrigger$1, window: DateWindow): Record<string, unknown>;
|
|
200
|
+
/**
|
|
201
|
+
* TimeRelativeTrigger
|
|
202
|
+
*
|
|
203
|
+
* The declarative answer to "act on records whose date field is coming up (or
|
|
204
|
+
* overdue)" (#1874). Instead of the fragile date-equality-on-record-change
|
|
205
|
+
* pattern (which only fires if the record happens to be edited on the threshold
|
|
206
|
+
* day) or a hand-rolled cron + range query per flow, a flow whose start node
|
|
207
|
+
* declares `config.timeRelative` is swept on a schedule (daily by default) and
|
|
208
|
+
* launched **once per matching record**.
|
|
209
|
+
*
|
|
210
|
+
* It composes the schedule trigger's two collaborators:
|
|
211
|
+
* - the platform {@link JobServiceSurface} owns the sweep cadence (like the
|
|
212
|
+
* plain schedule trigger), and
|
|
213
|
+
* - the {@link TimeRelativeDataEngine} runs the date-window query (like the
|
|
214
|
+
* record-change trigger reaching ObjectQL).
|
|
215
|
+
*
|
|
216
|
+
* Both are resolved lazily (per call) so adapter upgrades — the durable job
|
|
217
|
+
* adapter that replaces the bootstrap ticker on `kernel:ready`, a late-registered
|
|
218
|
+
* data engine — are always picked up. The engine owns the start-node `condition`
|
|
219
|
+
* gate and `runAs` identity, so this trigger only has to put the matched record
|
|
220
|
+
* on the {@link AutomationContext}; `{record.<field>}` interpolation and the
|
|
221
|
+
* condition work exactly as they do for a record-change flow.
|
|
222
|
+
*/
|
|
223
|
+
declare class TimeRelativeTrigger implements FlowTrigger {
|
|
224
|
+
readonly type = "time_relative";
|
|
225
|
+
private readonly getJobService;
|
|
226
|
+
private readonly getDataEngine;
|
|
227
|
+
private readonly logger;
|
|
228
|
+
/** Injectable clock so window math is deterministic under test. */
|
|
229
|
+
private readonly now;
|
|
230
|
+
/** flowName → job name registered for it, so stop() can cancel it. */
|
|
231
|
+
private readonly bound;
|
|
232
|
+
constructor(getJobService: () => JobServiceSurface | null, getDataEngine: () => TimeRelativeDataEngine | null, logger: TriggerLogger, now?: () => Date);
|
|
233
|
+
start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;
|
|
234
|
+
/**
|
|
235
|
+
* Run one sweep: query each date window, union the matched records (deduped
|
|
236
|
+
* by id, capped at `maxRecords`), and launch the flow once per record. A
|
|
237
|
+
* per-record failure is isolated so one bad row never aborts the batch.
|
|
238
|
+
*/
|
|
239
|
+
private sweep;
|
|
240
|
+
stop(flowName: string): void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { type DateWindow, type FlowTrigger, type FlowTriggerBinding, type JobServiceSurface, ScheduleTrigger, ScheduleTriggerPlugin, type TimeRelativeDataEngine, TimeRelativeTrigger, TimeRelativeTriggerPlugin, type TriggerLogger, buildWindowWhere, computeDateWindows, normalizeSchedule };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
2
2
|
import { AutomationContext, JobSchedule, JobHandler } from '@objectstack/spec/contracts';
|
|
3
|
+
import { TimeRelativeTrigger as TimeRelativeTrigger$1 } from '@objectstack/spec/automation';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* ScheduleTriggerPlugin
|
|
@@ -74,6 +75,14 @@ interface TriggerLogger {
|
|
|
74
75
|
info(msg: string, ...args: unknown[]): void;
|
|
75
76
|
warn(msg: string, ...args: unknown[]): void;
|
|
76
77
|
debug?(msg: string, ...args: unknown[]): void;
|
|
78
|
+
/**
|
|
79
|
+
* Execution failures log here when available (falling back to `warn`).
|
|
80
|
+
* ERROR matters operationally: the CLI's boot-quiet window swallows
|
|
81
|
+
* stdout (debug/info/warn) but stderr (error/fatal) always lands — so a
|
|
82
|
+
* per-record sweep failure stays visible. Mirrors the record-change
|
|
83
|
+
* trigger's logger surface.
|
|
84
|
+
*/
|
|
85
|
+
error?(msg: string, ...args: unknown[]): void;
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
79
88
|
* Normalize a flow's raw `schedule` descriptor into a {@link JobSchedule}, or
|
|
@@ -108,4 +117,127 @@ declare class ScheduleTrigger implements FlowTrigger {
|
|
|
108
117
|
stop(flowName: string): void;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
/**
|
|
121
|
+
* TimeRelativeTriggerPlugin
|
|
122
|
+
*
|
|
123
|
+
* Arms **declarative time-relative flows** (#1874): a flow whose start node
|
|
124
|
+
* declares `config.timeRelative` (object + dateField + `withinDays`/`offsetDays`)
|
|
125
|
+
* is swept on a schedule and launched once per record whose date field falls in
|
|
126
|
+
* the window — no hand-written cron + range query, no fragile
|
|
127
|
+
* date-equality-on-record-change.
|
|
128
|
+
*
|
|
129
|
+
* It ships in `@objectstack/trigger-schedule` alongside the plain schedule
|
|
130
|
+
* trigger (both are schedule-driven) but is a **separate** plugin: the
|
|
131
|
+
* time-relative trigger additionally needs the ObjectQL engine (for the sweep
|
|
132
|
+
* query), so keeping it separate leaves the plain `ScheduleTriggerPlugin`'s
|
|
133
|
+
* dependency surface unchanged. Depends on the job service (sweep cadence) and
|
|
134
|
+
* the ObjectQL engine (record discovery); both are resolved lazily per `start()`
|
|
135
|
+
* so adapter upgrades are always picked up.
|
|
136
|
+
*/
|
|
137
|
+
declare class TimeRelativeTriggerPlugin implements Plugin {
|
|
138
|
+
name: string;
|
|
139
|
+
type: string;
|
|
140
|
+
version: string;
|
|
141
|
+
dependencies: string[];
|
|
142
|
+
init(ctx: PluginContext): Promise<void>;
|
|
143
|
+
start(ctx: PluginContext): Promise<void>;
|
|
144
|
+
private resolveService;
|
|
145
|
+
private resolveDataEngine;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The slice of the ObjectQL data engine this trigger needs: run a filtered
|
|
150
|
+
* `find` (to discover the records whose date field falls in the window) and,
|
|
151
|
+
* optionally, probe whether an object is registered. Typed structurally — same
|
|
152
|
+
* decoupling pattern the record-change trigger uses for its hook surface — so
|
|
153
|
+
* this plugin does not take a build dependency on the engine package.
|
|
154
|
+
*/
|
|
155
|
+
interface TimeRelativeDataEngine {
|
|
156
|
+
find(objectName: string, query?: {
|
|
157
|
+
where?: Record<string, unknown>;
|
|
158
|
+
fields?: string[];
|
|
159
|
+
limit?: number;
|
|
160
|
+
/** Elevated context — a background sweep must see all rows, not RLS-scoped ones. */
|
|
161
|
+
context?: {
|
|
162
|
+
isSystem?: boolean;
|
|
163
|
+
};
|
|
164
|
+
}): Promise<Array<Record<string, unknown>> | undefined>;
|
|
165
|
+
/**
|
|
166
|
+
* Optional object-existence probe (the ObjectQL engine's `getObject`).
|
|
167
|
+
* When present, {@link TimeRelativeTrigger.start} uses it to call out a
|
|
168
|
+
* descriptor whose `object` matches no registered object at bind time —
|
|
169
|
+
* otherwise the sweep just quietly finds nothing forever.
|
|
170
|
+
*/
|
|
171
|
+
getObject?(name: string): unknown;
|
|
172
|
+
}
|
|
173
|
+
/** A closed, inclusive instant window `[gte, lte]` as ISO-8601 strings. */
|
|
174
|
+
interface DateWindow {
|
|
175
|
+
/** Lower bound (inclusive), ISO-8601. */
|
|
176
|
+
gte: string;
|
|
177
|
+
/** Upper bound (inclusive), ISO-8601. */
|
|
178
|
+
lte: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Compute the inclusive date window(s) a descriptor selects, relative to `now`.
|
|
182
|
+
*
|
|
183
|
+
* - `offsetDays` → one single-day window per offset (`today + offset`), so the
|
|
184
|
+
* sweep fires exactly on each threshold day (the robust T-minus reminder).
|
|
185
|
+
* - `withinDays` → one range window: `[today, today + N]` when N ≥ 0 (upcoming),
|
|
186
|
+
* or `[today − |N|, today]` when N < 0 (overdue lookback). Always includes today.
|
|
187
|
+
*
|
|
188
|
+
* Day-granular and computed in UTC. The upper bound is the *end* of its day
|
|
189
|
+
* (`23:59:59.999Z`), so a `datetime` field matches for the whole day and a
|
|
190
|
+
* `date` field (compared as `YYYY-MM-DD` after the driver truncates) is inclusive.
|
|
191
|
+
*/
|
|
192
|
+
declare function computeDateWindows(desc: TimeRelativeTrigger$1, now: Date): DateWindow[];
|
|
193
|
+
/**
|
|
194
|
+
* Build the ObjectQL `where` map for one date window: the descriptor's static
|
|
195
|
+
* `filter` (if any) ANDed with a `$gte`/`$lte` range on the date field. The map
|
|
196
|
+
* form is the canonical filter shape both drivers evaluate verbatim (the same
|
|
197
|
+
* shape the platform's own retention sweep uses).
|
|
198
|
+
*/
|
|
199
|
+
declare function buildWindowWhere(desc: TimeRelativeTrigger$1, window: DateWindow): Record<string, unknown>;
|
|
200
|
+
/**
|
|
201
|
+
* TimeRelativeTrigger
|
|
202
|
+
*
|
|
203
|
+
* The declarative answer to "act on records whose date field is coming up (or
|
|
204
|
+
* overdue)" (#1874). Instead of the fragile date-equality-on-record-change
|
|
205
|
+
* pattern (which only fires if the record happens to be edited on the threshold
|
|
206
|
+
* day) or a hand-rolled cron + range query per flow, a flow whose start node
|
|
207
|
+
* declares `config.timeRelative` is swept on a schedule (daily by default) and
|
|
208
|
+
* launched **once per matching record**.
|
|
209
|
+
*
|
|
210
|
+
* It composes the schedule trigger's two collaborators:
|
|
211
|
+
* - the platform {@link JobServiceSurface} owns the sweep cadence (like the
|
|
212
|
+
* plain schedule trigger), and
|
|
213
|
+
* - the {@link TimeRelativeDataEngine} runs the date-window query (like the
|
|
214
|
+
* record-change trigger reaching ObjectQL).
|
|
215
|
+
*
|
|
216
|
+
* Both are resolved lazily (per call) so adapter upgrades — the durable job
|
|
217
|
+
* adapter that replaces the bootstrap ticker on `kernel:ready`, a late-registered
|
|
218
|
+
* data engine — are always picked up. The engine owns the start-node `condition`
|
|
219
|
+
* gate and `runAs` identity, so this trigger only has to put the matched record
|
|
220
|
+
* on the {@link AutomationContext}; `{record.<field>}` interpolation and the
|
|
221
|
+
* condition work exactly as they do for a record-change flow.
|
|
222
|
+
*/
|
|
223
|
+
declare class TimeRelativeTrigger implements FlowTrigger {
|
|
224
|
+
readonly type = "time_relative";
|
|
225
|
+
private readonly getJobService;
|
|
226
|
+
private readonly getDataEngine;
|
|
227
|
+
private readonly logger;
|
|
228
|
+
/** Injectable clock so window math is deterministic under test. */
|
|
229
|
+
private readonly now;
|
|
230
|
+
/** flowName → job name registered for it, so stop() can cancel it. */
|
|
231
|
+
private readonly bound;
|
|
232
|
+
constructor(getJobService: () => JobServiceSurface | null, getDataEngine: () => TimeRelativeDataEngine | null, logger: TriggerLogger, now?: () => Date);
|
|
233
|
+
start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;
|
|
234
|
+
/**
|
|
235
|
+
* Run one sweep: query each date window, union the matched records (deduped
|
|
236
|
+
* by id, capped at `maxRecords`), and launch the flow once per record. A
|
|
237
|
+
* per-record failure is isolated so one bad row never aborts the batch.
|
|
238
|
+
*/
|
|
239
|
+
private sweep;
|
|
240
|
+
stop(flowName: string): void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { type DateWindow, type FlowTrigger, type FlowTriggerBinding, type JobServiceSurface, ScheduleTrigger, ScheduleTriggerPlugin, type TimeRelativeDataEngine, TimeRelativeTrigger, TimeRelativeTriggerPlugin, type TriggerLogger, buildWindowWhere, computeDateWindows, normalizeSchedule };
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,10 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ScheduleTrigger: () => ScheduleTrigger,
|
|
24
24
|
ScheduleTriggerPlugin: () => ScheduleTriggerPlugin,
|
|
25
|
+
TimeRelativeTrigger: () => TimeRelativeTrigger,
|
|
26
|
+
TimeRelativeTriggerPlugin: () => TimeRelativeTriggerPlugin,
|
|
27
|
+
buildWindowWhere: () => buildWindowWhere,
|
|
28
|
+
computeDateWindows: () => computeDateWindows,
|
|
25
29
|
normalizeSchedule: () => normalizeSchedule
|
|
26
30
|
});
|
|
27
31
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -166,10 +170,248 @@ var ScheduleTriggerPlugin = class {
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
};
|
|
173
|
+
|
|
174
|
+
// src/time-relative-trigger.ts
|
|
175
|
+
var import_automation = require("@objectstack/spec/automation");
|
|
176
|
+
var JOB_PREFIX2 = "flow-time-relative";
|
|
177
|
+
var MS_PER_DAY = 864e5;
|
|
178
|
+
function startOfUtcDay(d) {
|
|
179
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));
|
|
180
|
+
}
|
|
181
|
+
function endOfUtcDay(d) {
|
|
182
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 23, 59, 59, 999));
|
|
183
|
+
}
|
|
184
|
+
function addUtcDays(d, n) {
|
|
185
|
+
return new Date(startOfUtcDay(d).getTime() + n * MS_PER_DAY);
|
|
186
|
+
}
|
|
187
|
+
function computeDateWindows(desc, now) {
|
|
188
|
+
const today = startOfUtcDay(now);
|
|
189
|
+
if (desc.offsetDays && desc.offsetDays.length > 0) {
|
|
190
|
+
return desc.offsetDays.map((offset) => {
|
|
191
|
+
const day = addUtcDays(today, offset);
|
|
192
|
+
return { gte: startOfUtcDay(day).toISOString(), lte: endOfUtcDay(day).toISOString() };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const n = desc.withinDays ?? 0;
|
|
196
|
+
if (n >= 0) {
|
|
197
|
+
return [{ gte: startOfUtcDay(today).toISOString(), lte: endOfUtcDay(addUtcDays(today, n)).toISOString() }];
|
|
198
|
+
}
|
|
199
|
+
return [{ gte: startOfUtcDay(addUtcDays(today, n)).toISOString(), lte: endOfUtcDay(today).toISOString() }];
|
|
200
|
+
}
|
|
201
|
+
function buildWindowWhere(desc, window) {
|
|
202
|
+
return {
|
|
203
|
+
...desc.filter ?? {},
|
|
204
|
+
[desc.dateField]: { $gte: window.gte, $lte: window.lte }
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function errMessage(err) {
|
|
208
|
+
return err?.message ?? String(err);
|
|
209
|
+
}
|
|
210
|
+
var TimeRelativeTrigger = class {
|
|
211
|
+
constructor(getJobService, getDataEngine, logger, now = () => /* @__PURE__ */ new Date()) {
|
|
212
|
+
this.type = "time_relative";
|
|
213
|
+
/** flowName → job name registered for it, so stop() can cancel it. */
|
|
214
|
+
this.bound = /* @__PURE__ */ new Map();
|
|
215
|
+
this.getJobService = getJobService;
|
|
216
|
+
this.getDataEngine = getDataEngine;
|
|
217
|
+
this.logger = logger;
|
|
218
|
+
this.now = now;
|
|
219
|
+
}
|
|
220
|
+
start(binding, callback) {
|
|
221
|
+
const raw = binding.config?.timeRelative;
|
|
222
|
+
const parsed = import_automation.TimeRelativeTriggerSchema.safeParse(raw);
|
|
223
|
+
if (!parsed.success) {
|
|
224
|
+
this.logger.warn(
|
|
225
|
+
`[time-relative] flow '${binding.flowName}' has no valid \`timeRelative\` descriptor \u2014 not bound. Provide { object, dateField, and exactly one of withinDays | offsetDays }. (${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")})`
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const desc = parsed.data;
|
|
230
|
+
const schedule = normalizeSchedule(binding.schedule) ?? { type: "cron", expression: import_automation.TIME_RELATIVE_DEFAULT_CRON };
|
|
231
|
+
const jobService = this.getJobService();
|
|
232
|
+
if (!jobService || typeof jobService.schedule !== "function") {
|
|
233
|
+
this.logger.warn(
|
|
234
|
+
`[time-relative] job service unavailable \u2014 flow '${binding.flowName}' not scheduled`
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const engineNow = this.getDataEngine();
|
|
239
|
+
if (desc.object && engineNow && typeof engineNow.getObject === "function") {
|
|
240
|
+
let known;
|
|
241
|
+
try {
|
|
242
|
+
known = engineNow.getObject(desc.object);
|
|
243
|
+
} catch {
|
|
244
|
+
known = void 0;
|
|
245
|
+
}
|
|
246
|
+
if (!known) {
|
|
247
|
+
this.logger.warn(
|
|
248
|
+
`[time-relative] flow '${binding.flowName}' targets unknown object '${desc.object}' \u2014 the sweep is bound but will match nothing until that object is registered. Object names match exactly; check config.timeRelative.object.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.stop(binding.flowName);
|
|
253
|
+
const jobName = `${JOB_PREFIX2}:${binding.flowName}`;
|
|
254
|
+
const maxRecords = desc.maxRecords ?? import_automation.TIME_RELATIVE_DEFAULT_MAX_RECORDS;
|
|
255
|
+
const handler = async () => {
|
|
256
|
+
try {
|
|
257
|
+
await this.sweep(binding.flowName, desc, maxRecords, callback);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
this.logger.warn(
|
|
260
|
+
`[time-relative] flow '${binding.flowName}' sweep failed: ${errMessage(err)}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
this.bound.set(binding.flowName, jobName);
|
|
265
|
+
void Promise.resolve(jobService.schedule(jobName, schedule, handler)).then(() => {
|
|
266
|
+
const mode = desc.offsetDays ? `offsets [${desc.offsetDays.join(", ")}]d` : `within ${desc.withinDays}d`;
|
|
267
|
+
this.logger.info(
|
|
268
|
+
`[time-relative] bound flow '${binding.flowName}' \u2192 sweep '${desc.object}.${desc.dateField}' ${mode} on ${schedule.type}` + (schedule.expression ? ` '${schedule.expression}'` : "") + (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : "")
|
|
269
|
+
);
|
|
270
|
+
}).catch((err) => {
|
|
271
|
+
this.bound.delete(binding.flowName);
|
|
272
|
+
this.logger.warn(
|
|
273
|
+
`[time-relative] failed to schedule flow '${binding.flowName}': ${errMessage(err)}`
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Run one sweep: query each date window, union the matched records (deduped
|
|
279
|
+
* by id, capped at `maxRecords`), and launch the flow once per record. A
|
|
280
|
+
* per-record failure is isolated so one bad row never aborts the batch.
|
|
281
|
+
*/
|
|
282
|
+
async sweep(flowName, desc, maxRecords, callback) {
|
|
283
|
+
const engine = this.getDataEngine();
|
|
284
|
+
if (!engine || typeof engine.find !== "function") {
|
|
285
|
+
this.logger.warn(
|
|
286
|
+
`[time-relative] data engine unavailable \u2014 flow '${flowName}' sweep skipped this tick`
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const windows = computeDateWindows(desc, this.now());
|
|
291
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
292
|
+
const matched = [];
|
|
293
|
+
for (const window of windows) {
|
|
294
|
+
if (matched.length >= maxRecords) break;
|
|
295
|
+
const where = buildWindowWhere(desc, window);
|
|
296
|
+
const rows = await engine.find(desc.object, {
|
|
297
|
+
where,
|
|
298
|
+
limit: maxRecords,
|
|
299
|
+
context: { isSystem: true }
|
|
300
|
+
}) ?? [];
|
|
301
|
+
for (const row of rows) {
|
|
302
|
+
const id = row.id;
|
|
303
|
+
if (id != null) {
|
|
304
|
+
if (seenIds.has(id)) continue;
|
|
305
|
+
seenIds.add(id);
|
|
306
|
+
}
|
|
307
|
+
matched.push(row);
|
|
308
|
+
if (matched.length >= maxRecords) break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (matched.length >= maxRecords) {
|
|
312
|
+
this.logger.warn(
|
|
313
|
+
`[time-relative] flow '${flowName}' sweep hit the ${maxRecords}-record cap \u2014 some matching records were NOT processed this tick. Narrow the window/filter, or raise config.timeRelative.maxRecords.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
let launched = 0;
|
|
317
|
+
let failed = 0;
|
|
318
|
+
for (const record of matched) {
|
|
319
|
+
try {
|
|
320
|
+
const ctx = {
|
|
321
|
+
record,
|
|
322
|
+
object: desc.object,
|
|
323
|
+
event: "time_relative",
|
|
324
|
+
// Expose the record as params too, so flows with named `isInput`
|
|
325
|
+
// variables matching record fields get them seeded (parity with
|
|
326
|
+
// the record-change trigger).
|
|
327
|
+
params: record
|
|
328
|
+
};
|
|
329
|
+
await callback(ctx);
|
|
330
|
+
launched++;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
failed++;
|
|
333
|
+
const log = this.logger.error?.bind(this.logger) ?? this.logger.warn.bind(this.logger);
|
|
334
|
+
log(
|
|
335
|
+
`[time-relative] flow '${flowName}' failed for record '${String(record.id ?? "?")}': ${errMessage(err)}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.logger.debug?.(
|
|
340
|
+
`[time-relative] flow '${flowName}' swept '${desc.object}': ${matched.length} matched, ${launched} launched, ${failed} failed`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
stop(flowName) {
|
|
344
|
+
const jobName = this.bound.get(flowName);
|
|
345
|
+
if (!jobName) return;
|
|
346
|
+
this.bound.delete(flowName);
|
|
347
|
+
const jobService = this.getJobService();
|
|
348
|
+
if (!jobService || typeof jobService.cancel !== "function") return;
|
|
349
|
+
void Promise.resolve(jobService.cancel(jobName)).then(() => this.logger.debug?.(`[time-relative] unbound flow '${flowName}'`)).catch((err) => {
|
|
350
|
+
this.logger.warn(
|
|
351
|
+
`[time-relative] failed to unbind flow '${flowName}': ${errMessage(err)}`
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/time-relative-plugin.ts
|
|
358
|
+
var TimeRelativeTriggerPlugin = class {
|
|
359
|
+
constructor() {
|
|
360
|
+
this.name = "com.objectstack.trigger.time-relative";
|
|
361
|
+
this.type = "standard";
|
|
362
|
+
this.version = "1.0.0";
|
|
363
|
+
this.dependencies = ["com.objectstack.service.job", "com.objectstack.engine.objectql"];
|
|
364
|
+
}
|
|
365
|
+
async init(ctx) {
|
|
366
|
+
ctx.logger.info("Time-relative trigger plugin initialized");
|
|
367
|
+
}
|
|
368
|
+
async start(ctx) {
|
|
369
|
+
ctx.hook("kernel:ready", async () => {
|
|
370
|
+
const automation = this.resolveService(ctx, "automation");
|
|
371
|
+
if (!automation || typeof automation.registerTrigger !== "function") {
|
|
372
|
+
ctx.logger.warn(
|
|
373
|
+
"TimeRelativeTriggerPlugin: automation service not available \u2014 time-relative trigger NOT installed"
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!this.resolveService(ctx, "job")) {
|
|
378
|
+
ctx.logger.warn(
|
|
379
|
+
"TimeRelativeTriggerPlugin: job service not available \u2014 time-relative sweeps will not run until one is registered"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (!this.resolveDataEngine(ctx)) {
|
|
383
|
+
ctx.logger.warn(
|
|
384
|
+
"TimeRelativeTriggerPlugin: ObjectQL engine not available \u2014 time-relative sweeps will find no records until it is"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const trigger = new TimeRelativeTrigger(
|
|
388
|
+
() => this.resolveService(ctx, "job"),
|
|
389
|
+
() => this.resolveDataEngine(ctx),
|
|
390
|
+
ctx.logger
|
|
391
|
+
);
|
|
392
|
+
automation.registerTrigger(trigger);
|
|
393
|
+
ctx.logger.info("TimeRelativeTriggerPlugin: time-relative trigger registered");
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
resolveService(ctx, name) {
|
|
397
|
+
try {
|
|
398
|
+
return ctx.getService(name) ?? null;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
resolveDataEngine(ctx) {
|
|
404
|
+
return this.resolveService(ctx, "objectql") ?? this.resolveService(ctx, "data");
|
|
405
|
+
}
|
|
406
|
+
};
|
|
169
407
|
// Annotate the CommonJS export names for ESM import in node:
|
|
170
408
|
0 && (module.exports = {
|
|
171
409
|
ScheduleTrigger,
|
|
172
410
|
ScheduleTriggerPlugin,
|
|
411
|
+
TimeRelativeTrigger,
|
|
412
|
+
TimeRelativeTriggerPlugin,
|
|
413
|
+
buildWindowWhere,
|
|
414
|
+
computeDateWindows,
|
|
173
415
|
normalizeSchedule
|
|
174
416
|
});
|
|
175
417
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/schedule-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { ScheduleTriggerPlugin } from './plugin.js';\nexport { ScheduleTrigger, normalizeSchedule } from './schedule-trigger.js';\nexport type {\n FlowTrigger,\n FlowTriggerBinding,\n JobServiceSurface,\n TriggerLogger,\n} from './schedule-trigger.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AutomationContext } from '@objectstack/spec/contracts';\nimport type { JobSchedule, JobHandler } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding`\n * (service-automation/src/engine.ts). Declared locally so this trigger plugin\n * stays decoupled from the automation package — same pattern the record-change\n * trigger and the connector / messaging integrations use. The engine parses the\n * flow's start node and hands us a binding whose `schedule` carries the\n * cron/interval/once descriptor.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/**\n * Structural mirror of the engine's `FlowTrigger` extension point. The engine\n * calls {@link start} with a parsed binding + a callback that runs the flow,\n * and {@link stop} when the flow is unregistered/disabled.\n */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IJobService` this trigger needs: schedule a named job and\n * cancel it. Typed structurally so the plugin depends on the spec contract\n * shape, not a concrete adapter.\n */\nexport interface JobServiceSurface {\n schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;\n cancel(name: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst JOB_PREFIX = 'flow-schedule';\n\n/**\n * Normalize a flow's raw `schedule` descriptor into a {@link JobSchedule}, or\n * `null` if it can't be understood. Accepts the canonical\n * `{ type: 'cron'|'interval'|'once', ... }` shape plus a few ergonomic\n * shorthands (a bare cron string, `{ cron }`, `{ expression }`, `{ every }` /\n * `{ intervalMs }`, `{ at }`).\n */\nexport function normalizeSchedule(raw: unknown): JobSchedule | null {\n if (raw == null) return null;\n\n // Bare cron string, e.g. '0 1 * * *'.\n if (typeof raw === 'string') {\n const expr = raw.trim();\n return expr ? { type: 'cron', expression: expr } : null;\n }\n\n if (typeof raw !== 'object') return null;\n const s = raw as Record<string, unknown>;\n\n const type = typeof s.type === 'string' ? s.type : undefined;\n\n if (type === 'cron' || (!type && (typeof s.cron === 'string' || typeof s.expression === 'string'))) {\n const expression =\n (typeof s.expression === 'string' && s.expression) ||\n (typeof s.cron === 'string' && s.cron) ||\n undefined;\n if (!expression) return null;\n const out: JobSchedule = { type: 'cron', expression };\n if (typeof s.timezone === 'string') out.timezone = s.timezone;\n return out;\n }\n\n if (type === 'interval' || (!type && (typeof s.intervalMs === 'number' || typeof s.every === 'number'))) {\n const intervalMs =\n (typeof s.intervalMs === 'number' && s.intervalMs) ||\n (typeof s.every === 'number' && s.every) ||\n undefined;\n if (!intervalMs || intervalMs <= 0) return null;\n return { type: 'interval', intervalMs };\n }\n\n if (type === 'once' || (!type && typeof s.at === 'string')) {\n const at = typeof s.at === 'string' ? s.at : undefined;\n if (!at) return null;\n return { type: 'once', at };\n }\n\n return null;\n}\n\n/**\n * ScheduleTrigger\n *\n * Bridges the automation engine's {@link FlowTrigger} extension point to the\n * platform {@link JobServiceSurface}. For each schedule-triggered flow the\n * engine activates, it registers a job whose handler runs the flow; the job\n * service owns the actual cron/interval/once timing (so this trigger stays\n * adapter-agnostic — cron schedules need a cron-capable adapter, which the\n * job service selects).\n *\n * The job service is resolved lazily (per `start()`) via the supplied accessor,\n * so we always pick up the job service's *upgraded* adapter (e.g. the durable\n * DbJobAdapter that replaces the bootstrap interval adapter on `kernel:ready`).\n */\nexport class ScheduleTrigger implements FlowTrigger {\n readonly type = 'schedule';\n\n private readonly getJobService: () => JobServiceSurface | null;\n private readonly logger: TriggerLogger;\n /** flowName → job name registered for it, so stop() can cancel it. */\n private readonly bound = new Map<string, string>();\n\n constructor(getJobService: () => JobServiceSurface | null, logger: TriggerLogger) {\n this.getJobService = getJobService;\n this.logger = logger;\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const raw = binding.schedule ?? (binding.config as Record<string, unknown> | undefined)?.schedule;\n const schedule = normalizeSchedule(raw);\n if (!schedule) {\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' has no recognizable schedule descriptor — not bound`,\n );\n return;\n }\n\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.schedule !== 'function') {\n this.logger.warn(\n `[schedule] job service unavailable — flow '${binding.flowName}' not scheduled`,\n );\n return;\n }\n\n // Idempotent: drop any prior schedule for this flow before re-binding\n // (covers disable→enable cycles and hot reload).\n this.stop(binding.flowName);\n\n const jobName = `${JOB_PREFIX}:${binding.flowName}`;\n\n const handler: JobHandler = async ({ jobId }) => {\n try {\n const ctx: AutomationContext = {\n event: 'schedule',\n params: {\n jobId,\n flowName: binding.flowName,\n schedule,\n },\n };\n await callback(ctx);\n } catch (err) {\n // Error isolation: a scheduled flow failure must not crash the\n // job runner / ticker. Log and swallow.\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' execution failed: ${(err as Error)?.message ?? String(err)}`,\n );\n }\n };\n\n this.bound.set(binding.flowName, jobName);\n // FlowTrigger.start is sync; the job service's schedule() is async.\n // Fire-and-forget with error logging.\n void Promise.resolve(jobService.schedule(jobName, schedule, handler))\n .then(() => {\n this.logger.info(\n `[schedule] bound flow '${binding.flowName}' → ${schedule.type}` +\n (schedule.expression ? ` '${schedule.expression}'` : '') +\n (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : '') +\n (schedule.at ? ` at ${schedule.at}` : ''),\n );\n })\n .catch((err) => {\n this.bound.delete(binding.flowName);\n this.logger.warn(\n `[schedule] failed to schedule flow '${binding.flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n\n stop(flowName: string): void {\n const jobName = this.bound.get(flowName);\n if (!jobName) return;\n this.bound.delete(flowName);\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.cancel !== 'function') return;\n void Promise.resolve(jobService.cancel(jobName))\n .then(() => this.logger.debug?.(`[schedule] unbound flow '${flowName}'`))\n .catch((err) => {\n this.logger.warn(\n `[schedule] failed to unbind flow '${flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ScheduleTrigger } from './schedule-trigger.js';\nimport type { FlowTrigger, JobServiceSurface } from './schedule-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs: register a trigger on\n * its `FlowTrigger` extension point. Declared structurally so the plugin does\n * not take a build dependency on `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/**\n * ScheduleTriggerPlugin\n *\n * Makes schedule-triggered flows actually fire. The automation engine ships the\n * `FlowTrigger` wiring (it parses each flow's start node — `flow.type ===\n * 'schedule'` or a start-node `config.schedule` descriptor — into a binding and\n * calls `trigger.start(...)`), but the *concrete* schedule trigger lives here as\n * a plugin and delegates timing to the platform `IJobService` (the `'job'`\n * service). This mirrors the connector / record-change split (engine baseline +\n * trigger plugin).\n *\n * With this plugin (and a job service) installed, a flow whose start node\n * declares `config: { schedule: { type: 'cron', expression: '0 1 * * *' } }`\n * auto-launches on that schedule — no manual `engine.execute()`.\n *\n * Depends on the job service plugin so its `kernel:ready` upgrade (to the\n * durable DbJobAdapter) runs before ours; the job service is nonetheless\n * resolved lazily per `start()` so we always use its current adapter.\n */\nexport class ScheduleTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.schedule';\n type = 'standard';\n version = '7.3.0';\n dependencies = ['com.objectstack.service.job'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Schedule trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // The automation service + job service are resolvable once the kernel is\n // ready (kernel:ready fires after AutomationServicePlugin.start() has\n // pulled flows in and after the job service upgrades its adapter).\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: automation service not available — schedule trigger NOT installed',\n );\n return;\n }\n\n // Probe once for a clear startup warning; the trigger re-resolves\n // lazily on each start() so adapter upgrades are always picked up.\n if (!this.resolveService<JobServiceSurface>(ctx, 'job')) {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: job service not available — scheduled flows will not run until one is registered',\n );\n }\n\n const trigger = new ScheduleTrigger(\n () => this.resolveService<JobServiceSurface>(ctx, 'job'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n ctx.logger.info('ScheduleTriggerPlugin: schedule trigger registered');\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/schedule-trigger.ts","../src/plugin.ts","../src/time-relative-trigger.ts","../src/time-relative-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { ScheduleTriggerPlugin } from './plugin.js';\nexport { ScheduleTrigger, normalizeSchedule } from './schedule-trigger.js';\nexport type {\n FlowTrigger,\n FlowTriggerBinding,\n JobServiceSurface,\n TriggerLogger,\n} from './schedule-trigger.js';\n\nexport { TimeRelativeTriggerPlugin } from './time-relative-plugin.js';\nexport {\n TimeRelativeTrigger,\n computeDateWindows,\n buildWindowWhere,\n} from './time-relative-trigger.js';\nexport type { TimeRelativeDataEngine, DateWindow } from './time-relative-trigger.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AutomationContext } from '@objectstack/spec/contracts';\nimport type { JobSchedule, JobHandler } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding`\n * (service-automation/src/engine.ts). Declared locally so this trigger plugin\n * stays decoupled from the automation package — same pattern the record-change\n * trigger and the connector / messaging integrations use. The engine parses the\n * flow's start node and hands us a binding whose `schedule` carries the\n * cron/interval/once descriptor.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/**\n * Structural mirror of the engine's `FlowTrigger` extension point. The engine\n * calls {@link start} with a parsed binding + a callback that runs the flow,\n * and {@link stop} when the flow is unregistered/disabled.\n */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IJobService` this trigger needs: schedule a named job and\n * cancel it. Typed structurally so the plugin depends on the spec contract\n * shape, not a concrete adapter.\n */\nexport interface JobServiceSurface {\n schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;\n cancel(name: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n /**\n * Execution failures log here when available (falling back to `warn`).\n * ERROR matters operationally: the CLI's boot-quiet window swallows\n * stdout (debug/info/warn) but stderr (error/fatal) always lands — so a\n * per-record sweep failure stays visible. Mirrors the record-change\n * trigger's logger surface.\n */\n error?(msg: string, ...args: unknown[]): void;\n}\n\nconst JOB_PREFIX = 'flow-schedule';\n\n/**\n * Normalize a flow's raw `schedule` descriptor into a {@link JobSchedule}, or\n * `null` if it can't be understood. Accepts the canonical\n * `{ type: 'cron'|'interval'|'once', ... }` shape plus a few ergonomic\n * shorthands (a bare cron string, `{ cron }`, `{ expression }`, `{ every }` /\n * `{ intervalMs }`, `{ at }`).\n */\nexport function normalizeSchedule(raw: unknown): JobSchedule | null {\n if (raw == null) return null;\n\n // Bare cron string, e.g. '0 1 * * *'.\n if (typeof raw === 'string') {\n const expr = raw.trim();\n return expr ? { type: 'cron', expression: expr } : null;\n }\n\n if (typeof raw !== 'object') return null;\n const s = raw as Record<string, unknown>;\n\n const type = typeof s.type === 'string' ? s.type : undefined;\n\n if (type === 'cron' || (!type && (typeof s.cron === 'string' || typeof s.expression === 'string'))) {\n const expression =\n (typeof s.expression === 'string' && s.expression) ||\n (typeof s.cron === 'string' && s.cron) ||\n undefined;\n if (!expression) return null;\n const out: JobSchedule = { type: 'cron', expression };\n if (typeof s.timezone === 'string') out.timezone = s.timezone;\n return out;\n }\n\n if (type === 'interval' || (!type && (typeof s.intervalMs === 'number' || typeof s.every === 'number'))) {\n const intervalMs =\n (typeof s.intervalMs === 'number' && s.intervalMs) ||\n (typeof s.every === 'number' && s.every) ||\n undefined;\n if (!intervalMs || intervalMs <= 0) return null;\n return { type: 'interval', intervalMs };\n }\n\n if (type === 'once' || (!type && typeof s.at === 'string')) {\n const at = typeof s.at === 'string' ? s.at : undefined;\n if (!at) return null;\n return { type: 'once', at };\n }\n\n return null;\n}\n\n/**\n * ScheduleTrigger\n *\n * Bridges the automation engine's {@link FlowTrigger} extension point to the\n * platform {@link JobServiceSurface}. For each schedule-triggered flow the\n * engine activates, it registers a job whose handler runs the flow; the job\n * service owns the actual cron/interval/once timing (so this trigger stays\n * adapter-agnostic — cron schedules need a cron-capable adapter, which the\n * job service selects).\n *\n * The job service is resolved lazily (per `start()`) via the supplied accessor,\n * so we always pick up the job service's *upgraded* adapter (e.g. the durable\n * DbJobAdapter that replaces the bootstrap interval adapter on `kernel:ready`).\n */\nexport class ScheduleTrigger implements FlowTrigger {\n readonly type = 'schedule';\n\n private readonly getJobService: () => JobServiceSurface | null;\n private readonly logger: TriggerLogger;\n /** flowName → job name registered for it, so stop() can cancel it. */\n private readonly bound = new Map<string, string>();\n\n constructor(getJobService: () => JobServiceSurface | null, logger: TriggerLogger) {\n this.getJobService = getJobService;\n this.logger = logger;\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const raw = binding.schedule ?? (binding.config as Record<string, unknown> | undefined)?.schedule;\n const schedule = normalizeSchedule(raw);\n if (!schedule) {\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' has no recognizable schedule descriptor — not bound`,\n );\n return;\n }\n\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.schedule !== 'function') {\n this.logger.warn(\n `[schedule] job service unavailable — flow '${binding.flowName}' not scheduled`,\n );\n return;\n }\n\n // Idempotent: drop any prior schedule for this flow before re-binding\n // (covers disable→enable cycles and hot reload).\n this.stop(binding.flowName);\n\n const jobName = `${JOB_PREFIX}:${binding.flowName}`;\n\n const handler: JobHandler = async ({ jobId }) => {\n try {\n const ctx: AutomationContext = {\n event: 'schedule',\n params: {\n jobId,\n flowName: binding.flowName,\n schedule,\n },\n };\n await callback(ctx);\n } catch (err) {\n // Error isolation: a scheduled flow failure must not crash the\n // job runner / ticker. Log and swallow.\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' execution failed: ${(err as Error)?.message ?? String(err)}`,\n );\n }\n };\n\n this.bound.set(binding.flowName, jobName);\n // FlowTrigger.start is sync; the job service's schedule() is async.\n // Fire-and-forget with error logging.\n void Promise.resolve(jobService.schedule(jobName, schedule, handler))\n .then(() => {\n this.logger.info(\n `[schedule] bound flow '${binding.flowName}' → ${schedule.type}` +\n (schedule.expression ? ` '${schedule.expression}'` : '') +\n (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : '') +\n (schedule.at ? ` at ${schedule.at}` : ''),\n );\n })\n .catch((err) => {\n this.bound.delete(binding.flowName);\n this.logger.warn(\n `[schedule] failed to schedule flow '${binding.flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n\n stop(flowName: string): void {\n const jobName = this.bound.get(flowName);\n if (!jobName) return;\n this.bound.delete(flowName);\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.cancel !== 'function') return;\n void Promise.resolve(jobService.cancel(jobName))\n .then(() => this.logger.debug?.(`[schedule] unbound flow '${flowName}'`))\n .catch((err) => {\n this.logger.warn(\n `[schedule] failed to unbind flow '${flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ScheduleTrigger } from './schedule-trigger.js';\nimport type { FlowTrigger, JobServiceSurface } from './schedule-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs: register a trigger on\n * its `FlowTrigger` extension point. Declared structurally so the plugin does\n * not take a build dependency on `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/**\n * ScheduleTriggerPlugin\n *\n * Makes schedule-triggered flows actually fire. The automation engine ships the\n * `FlowTrigger` wiring (it parses each flow's start node — `flow.type ===\n * 'schedule'` or a start-node `config.schedule` descriptor — into a binding and\n * calls `trigger.start(...)`), but the *concrete* schedule trigger lives here as\n * a plugin and delegates timing to the platform `IJobService` (the `'job'`\n * service). This mirrors the connector / record-change split (engine baseline +\n * trigger plugin).\n *\n * With this plugin (and a job service) installed, a flow whose start node\n * declares `config: { schedule: { type: 'cron', expression: '0 1 * * *' } }`\n * auto-launches on that schedule — no manual `engine.execute()`.\n *\n * Depends on the job service plugin so its `kernel:ready` upgrade (to the\n * durable DbJobAdapter) runs before ours; the job service is nonetheless\n * resolved lazily per `start()` so we always use its current adapter.\n */\nexport class ScheduleTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.schedule';\n type = 'standard';\n version = '7.3.0';\n dependencies = ['com.objectstack.service.job'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Schedule trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // The automation service + job service are resolvable once the kernel is\n // ready (kernel:ready fires after AutomationServicePlugin.start() has\n // pulled flows in and after the job service upgrades its adapter).\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: automation service not available — schedule trigger NOT installed',\n );\n return;\n }\n\n // Probe once for a clear startup warning; the trigger re-resolves\n // lazily on each start() so adapter upgrades are always picked up.\n if (!this.resolveService<JobServiceSurface>(ctx, 'job')) {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: job service not available — scheduled flows will not run until one is registered',\n );\n }\n\n const trigger = new ScheduleTrigger(\n () => this.resolveService<JobServiceSurface>(ctx, 'job'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n ctx.logger.info('ScheduleTriggerPlugin: schedule trigger registered');\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AutomationContext, JobSchedule, JobHandler } from '@objectstack/spec/contracts';\nimport {\n TimeRelativeTriggerSchema,\n TIME_RELATIVE_DEFAULT_CRON,\n TIME_RELATIVE_DEFAULT_MAX_RECORDS,\n} from '@objectstack/spec/automation';\nimport type { TimeRelativeTrigger as TimeRelativeDescriptor } from '@objectstack/spec/automation';\nimport { normalizeSchedule } from './schedule-trigger.js';\nimport type { FlowTrigger, FlowTriggerBinding, JobServiceSurface, TriggerLogger } from './schedule-trigger.js';\n\n/**\n * The slice of the ObjectQL data engine this trigger needs: run a filtered\n * `find` (to discover the records whose date field falls in the window) and,\n * optionally, probe whether an object is registered. Typed structurally — same\n * decoupling pattern the record-change trigger uses for its hook surface — so\n * this plugin does not take a build dependency on the engine package.\n */\nexport interface TimeRelativeDataEngine {\n find(\n objectName: string,\n query?: {\n where?: Record<string, unknown>;\n fields?: string[];\n limit?: number;\n /** Elevated context — a background sweep must see all rows, not RLS-scoped ones. */\n context?: { isSystem?: boolean };\n },\n ): Promise<Array<Record<string, unknown>> | undefined>;\n /**\n * Optional object-existence probe (the ObjectQL engine's `getObject`).\n * When present, {@link TimeRelativeTrigger.start} uses it to call out a\n * descriptor whose `object` matches no registered object at bind time —\n * otherwise the sweep just quietly finds nothing forever.\n */\n getObject?(name: string): unknown;\n}\n\n/** Job-name namespace so time-relative sweeps never collide with plain schedule jobs. */\nconst JOB_PREFIX = 'flow-time-relative';\n\nconst MS_PER_DAY = 86_400_000;\n\n/** A closed, inclusive instant window `[gte, lte]` as ISO-8601 strings. */\nexport interface DateWindow {\n /** Lower bound (inclusive), ISO-8601. */\n gte: string;\n /** Upper bound (inclusive), ISO-8601. */\n lte: string;\n}\n\n// ─── Pure window math (day-granular, UTC) ───────────────────────────\n\n/** Start of `d`'s UTC calendar day (00:00:00.000Z). */\nfunction startOfUtcDay(d: Date): Date {\n return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));\n}\n\n/** End of `d`'s UTC calendar day (23:59:59.999Z) — inclusive upper bound. */\nfunction endOfUtcDay(d: Date): Date {\n return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 23, 59, 59, 999));\n}\n\n/** `d`'s UTC day shifted by `n` whole days (exact in UTC — no DST drift). */\nfunction addUtcDays(d: Date, n: number): Date {\n return new Date(startOfUtcDay(d).getTime() + n * MS_PER_DAY);\n}\n\n/**\n * Compute the inclusive date window(s) a descriptor selects, relative to `now`.\n *\n * - `offsetDays` → one single-day window per offset (`today + offset`), so the\n * sweep fires exactly on each threshold day (the robust T-minus reminder).\n * - `withinDays` → one range window: `[today, today + N]` when N ≥ 0 (upcoming),\n * or `[today − |N|, today]` when N < 0 (overdue lookback). Always includes today.\n *\n * Day-granular and computed in UTC. The upper bound is the *end* of its day\n * (`23:59:59.999Z`), so a `datetime` field matches for the whole day and a\n * `date` field (compared as `YYYY-MM-DD` after the driver truncates) is inclusive.\n */\nexport function computeDateWindows(desc: TimeRelativeDescriptor, now: Date): DateWindow[] {\n const today = startOfUtcDay(now);\n\n if (desc.offsetDays && desc.offsetDays.length > 0) {\n return desc.offsetDays.map((offset) => {\n const day = addUtcDays(today, offset);\n return { gte: startOfUtcDay(day).toISOString(), lte: endOfUtcDay(day).toISOString() };\n });\n }\n\n const n = desc.withinDays ?? 0;\n if (n >= 0) {\n return [{ gte: startOfUtcDay(today).toISOString(), lte: endOfUtcDay(addUtcDays(today, n)).toISOString() }];\n }\n // Negative: window extends into the past, still anchored to (and including) today.\n return [{ gte: startOfUtcDay(addUtcDays(today, n)).toISOString(), lte: endOfUtcDay(today).toISOString() }];\n}\n\n/**\n * Build the ObjectQL `where` map for one date window: the descriptor's static\n * `filter` (if any) ANDed with a `$gte`/`$lte` range on the date field. The map\n * form is the canonical filter shape both drivers evaluate verbatim (the same\n * shape the platform's own retention sweep uses).\n */\nexport function buildWindowWhere(desc: TimeRelativeDescriptor, window: DateWindow): Record<string, unknown> {\n return {\n ...(desc.filter ?? {}),\n [desc.dateField]: { $gte: window.gte, $lte: window.lte },\n };\n}\n\nfunction errMessage(err: unknown): string {\n return (err as Error)?.message ?? String(err);\n}\n\n/**\n * TimeRelativeTrigger\n *\n * The declarative answer to \"act on records whose date field is coming up (or\n * overdue)\" (#1874). Instead of the fragile date-equality-on-record-change\n * pattern (which only fires if the record happens to be edited on the threshold\n * day) or a hand-rolled cron + range query per flow, a flow whose start node\n * declares `config.timeRelative` is swept on a schedule (daily by default) and\n * launched **once per matching record**.\n *\n * It composes the schedule trigger's two collaborators:\n * - the platform {@link JobServiceSurface} owns the sweep cadence (like the\n * plain schedule trigger), and\n * - the {@link TimeRelativeDataEngine} runs the date-window query (like the\n * record-change trigger reaching ObjectQL).\n *\n * Both are resolved lazily (per call) so adapter upgrades — the durable job\n * adapter that replaces the bootstrap ticker on `kernel:ready`, a late-registered\n * data engine — are always picked up. The engine owns the start-node `condition`\n * gate and `runAs` identity, so this trigger only has to put the matched record\n * on the {@link AutomationContext}; `{record.<field>}` interpolation and the\n * condition work exactly as they do for a record-change flow.\n */\nexport class TimeRelativeTrigger implements FlowTrigger {\n readonly type = 'time_relative';\n\n private readonly getJobService: () => JobServiceSurface | null;\n private readonly getDataEngine: () => TimeRelativeDataEngine | null;\n private readonly logger: TriggerLogger;\n /** Injectable clock so window math is deterministic under test. */\n private readonly now: () => Date;\n /** flowName → job name registered for it, so stop() can cancel it. */\n private readonly bound = new Map<string, string>();\n\n constructor(\n getJobService: () => JobServiceSurface | null,\n getDataEngine: () => TimeRelativeDataEngine | null,\n logger: TriggerLogger,\n now: () => Date = () => new Date(),\n ) {\n this.getJobService = getJobService;\n this.getDataEngine = getDataEngine;\n this.logger = logger;\n this.now = now;\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const raw = (binding.config as Record<string, unknown> | undefined)?.timeRelative;\n const parsed = TimeRelativeTriggerSchema.safeParse(raw);\n if (!parsed.success) {\n this.logger.warn(\n `[time-relative] flow '${binding.flowName}' has no valid \\`timeRelative\\` descriptor — not bound. ` +\n `Provide { object, dateField, and exactly one of withinDays | offsetDays }. ` +\n `(${parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ')})`,\n );\n return;\n }\n const desc = parsed.data;\n\n // Cadence: the flow's start-node schedule descriptor, or a daily default.\n // A daily sweep is the whole point (evaluate the window every day so a\n // threshold day is never missed), so an omitted schedule means \"daily\",\n // not \"never\".\n const schedule: JobSchedule =\n normalizeSchedule(binding.schedule) ?? { type: 'cron', expression: TIME_RELATIVE_DEFAULT_CRON };\n\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.schedule !== 'function') {\n this.logger.warn(\n `[time-relative] job service unavailable — flow '${binding.flowName}' not scheduled`,\n );\n return;\n }\n\n // Best-effort object-existence probe at bind time (the engine may be\n // available now even though the sweep resolves it lazily). A descriptor\n // targeting an unknown object would sweep forever finding nothing.\n const engineNow = this.getDataEngine();\n if (desc.object && engineNow && typeof engineNow.getObject === 'function') {\n let known: unknown;\n try {\n known = engineNow.getObject(desc.object);\n } catch {\n known = undefined;\n }\n if (!known) {\n this.logger.warn(\n `[time-relative] flow '${binding.flowName}' targets unknown object '${desc.object}' — the sweep is bound but will match nothing until that object is registered. ` +\n `Object names match exactly; check config.timeRelative.object.`,\n );\n }\n }\n\n // Idempotent: drop any prior schedule for this flow before re-binding\n // (covers disable→enable cycles and hot reload).\n this.stop(binding.flowName);\n\n const jobName = `${JOB_PREFIX}:${binding.flowName}`;\n const maxRecords = desc.maxRecords ?? TIME_RELATIVE_DEFAULT_MAX_RECORDS;\n\n const handler: JobHandler = async () => {\n try {\n await this.sweep(binding.flowName, desc, maxRecords, callback);\n } catch (err) {\n // Error isolation: a sweep failure must not crash the job\n // runner / ticker. Log and swallow.\n this.logger.warn(\n `[time-relative] flow '${binding.flowName}' sweep failed: ${errMessage(err)}`,\n );\n }\n };\n\n this.bound.set(binding.flowName, jobName);\n // FlowTrigger.start is sync; the job service's schedule() is async.\n // Fire-and-forget with error logging (mirrors ScheduleTrigger).\n void Promise.resolve(jobService.schedule(jobName, schedule, handler))\n .then(() => {\n const mode = desc.offsetDays\n ? `offsets [${desc.offsetDays.join(', ')}]d`\n : `within ${desc.withinDays}d`;\n this.logger.info(\n `[time-relative] bound flow '${binding.flowName}' → sweep '${desc.object}.${desc.dateField}' ${mode} on ${schedule.type}` +\n (schedule.expression ? ` '${schedule.expression}'` : '') +\n (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : ''),\n );\n })\n .catch((err) => {\n this.bound.delete(binding.flowName);\n this.logger.warn(\n `[time-relative] failed to schedule flow '${binding.flowName}': ${errMessage(err)}`,\n );\n });\n }\n\n /**\n * Run one sweep: query each date window, union the matched records (deduped\n * by id, capped at `maxRecords`), and launch the flow once per record. A\n * per-record failure is isolated so one bad row never aborts the batch.\n */\n private async sweep(\n flowName: string,\n desc: TimeRelativeDescriptor,\n maxRecords: number,\n callback: (ctx: AutomationContext) => Promise<void>,\n ): Promise<void> {\n const engine = this.getDataEngine();\n if (!engine || typeof engine.find !== 'function') {\n this.logger.warn(\n `[time-relative] data engine unavailable — flow '${flowName}' sweep skipped this tick`,\n );\n return;\n }\n\n const windows = computeDateWindows(desc, this.now());\n const seenIds = new Set<unknown>();\n const matched: Array<Record<string, unknown>> = [];\n\n for (const window of windows) {\n if (matched.length >= maxRecords) break;\n const where = buildWindowWhere(desc, window);\n const rows =\n (await engine.find(desc.object, {\n where,\n limit: maxRecords,\n context: { isSystem: true },\n })) ?? [];\n for (const row of rows) {\n const id = (row as { id?: unknown }).id;\n // Dedup across windows (offset mode) by id; rows without an id\n // are always kept (can't dedup, better than dropping).\n if (id != null) {\n if (seenIds.has(id)) continue;\n seenIds.add(id);\n }\n matched.push(row);\n if (matched.length >= maxRecords) break;\n }\n }\n\n if (matched.length >= maxRecords) {\n this.logger.warn(\n `[time-relative] flow '${flowName}' sweep hit the ${maxRecords}-record cap — some matching records were NOT processed this tick. ` +\n `Narrow the window/filter, or raise config.timeRelative.maxRecords.`,\n );\n }\n\n let launched = 0;\n let failed = 0;\n for (const record of matched) {\n try {\n const ctx: AutomationContext = {\n record,\n object: desc.object,\n event: 'time_relative',\n // Expose the record as params too, so flows with named `isInput`\n // variables matching record fields get them seeded (parity with\n // the record-change trigger).\n params: record,\n };\n await callback(ctx);\n launched++;\n } catch (err) {\n failed++;\n // Error isolation per record: one failing flow run must not stop\n // the sweep. ERROR when available (stderr survives the CLI's\n // boot-quiet stdout window), else warn.\n const log = this.logger.error?.bind(this.logger) ?? this.logger.warn.bind(this.logger);\n log(\n `[time-relative] flow '${flowName}' failed for record '${String((record as { id?: unknown }).id ?? '?')}': ${errMessage(err)}`,\n );\n }\n }\n\n this.logger.debug?.(\n `[time-relative] flow '${flowName}' swept '${desc.object}': ${matched.length} matched, ${launched} launched, ${failed} failed`,\n );\n }\n\n stop(flowName: string): void {\n const jobName = this.bound.get(flowName);\n if (!jobName) return;\n this.bound.delete(flowName);\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.cancel !== 'function') return;\n void Promise.resolve(jobService.cancel(jobName))\n .then(() => this.logger.debug?.(`[time-relative] unbound flow '${flowName}'`))\n .catch((err) => {\n this.logger.warn(\n `[time-relative] failed to unbind flow '${flowName}': ${errMessage(err)}`,\n );\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { TimeRelativeTrigger } from './time-relative-trigger.js';\nimport type { TimeRelativeDataEngine } from './time-relative-trigger.js';\nimport type { FlowTrigger, JobServiceSurface } from './schedule-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs: register a trigger on\n * its `FlowTrigger` extension point. Declared structurally so the plugin does\n * not take a build dependency on `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/**\n * TimeRelativeTriggerPlugin\n *\n * Arms **declarative time-relative flows** (#1874): a flow whose start node\n * declares `config.timeRelative` (object + dateField + `withinDays`/`offsetDays`)\n * is swept on a schedule and launched once per record whose date field falls in\n * the window — no hand-written cron + range query, no fragile\n * date-equality-on-record-change.\n *\n * It ships in `@objectstack/trigger-schedule` alongside the plain schedule\n * trigger (both are schedule-driven) but is a **separate** plugin: the\n * time-relative trigger additionally needs the ObjectQL engine (for the sweep\n * query), so keeping it separate leaves the plain `ScheduleTriggerPlugin`'s\n * dependency surface unchanged. Depends on the job service (sweep cadence) and\n * the ObjectQL engine (record discovery); both are resolved lazily per `start()`\n * so adapter upgrades are always picked up.\n */\nexport class TimeRelativeTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.time-relative';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.service.job', 'com.objectstack.engine.objectql'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Time-relative trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // The automation service, job service, and ObjectQL engine are all\n // resolvable once the kernel is ready (kernel:ready fires after\n // AutomationServicePlugin.start() has pulled flows in and after the job\n // service upgrades its adapter).\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn(\n 'TimeRelativeTriggerPlugin: automation service not available — time-relative trigger NOT installed',\n );\n return;\n }\n\n // Probe once for a clear startup warning; the trigger re-resolves\n // both collaborators lazily on each start()/sweep so late upgrades\n // are always picked up.\n if (!this.resolveService<JobServiceSurface>(ctx, 'job')) {\n ctx.logger.warn(\n 'TimeRelativeTriggerPlugin: job service not available — time-relative sweeps will not run until one is registered',\n );\n }\n if (!this.resolveDataEngine(ctx)) {\n ctx.logger.warn(\n 'TimeRelativeTriggerPlugin: ObjectQL engine not available — time-relative sweeps will find no records until it is',\n );\n }\n\n const trigger = new TimeRelativeTrigger(\n () => this.resolveService<JobServiceSurface>(ctx, 'job'),\n () => this.resolveDataEngine(ctx),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n ctx.logger.info('TimeRelativeTriggerPlugin: time-relative trigger registered');\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n\n private resolveDataEngine(ctx: PluginContext): TimeRelativeDataEngine | null {\n // Primary alias 'objectql', fallback 'data' (some kernels register the\n // engine under both) — same lookup the record-change trigger uses.\n return (\n this.resolveService<TimeRelativeDataEngine>(ctx, 'objectql') ??\n this.resolveService<TimeRelativeDataEngine>(ctx, 'data')\n );\n }\n}\n"],"mappings":"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","names":["JOB_PREFIX"]}
|
package/dist/index.mjs
CHANGED
|
@@ -138,9 +138,251 @@ var ScheduleTriggerPlugin = class {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
|
+
|
|
142
|
+
// src/time-relative-trigger.ts
|
|
143
|
+
import {
|
|
144
|
+
TimeRelativeTriggerSchema,
|
|
145
|
+
TIME_RELATIVE_DEFAULT_CRON,
|
|
146
|
+
TIME_RELATIVE_DEFAULT_MAX_RECORDS
|
|
147
|
+
} from "@objectstack/spec/automation";
|
|
148
|
+
var JOB_PREFIX2 = "flow-time-relative";
|
|
149
|
+
var MS_PER_DAY = 864e5;
|
|
150
|
+
function startOfUtcDay(d) {
|
|
151
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));
|
|
152
|
+
}
|
|
153
|
+
function endOfUtcDay(d) {
|
|
154
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 23, 59, 59, 999));
|
|
155
|
+
}
|
|
156
|
+
function addUtcDays(d, n) {
|
|
157
|
+
return new Date(startOfUtcDay(d).getTime() + n * MS_PER_DAY);
|
|
158
|
+
}
|
|
159
|
+
function computeDateWindows(desc, now) {
|
|
160
|
+
const today = startOfUtcDay(now);
|
|
161
|
+
if (desc.offsetDays && desc.offsetDays.length > 0) {
|
|
162
|
+
return desc.offsetDays.map((offset) => {
|
|
163
|
+
const day = addUtcDays(today, offset);
|
|
164
|
+
return { gte: startOfUtcDay(day).toISOString(), lte: endOfUtcDay(day).toISOString() };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const n = desc.withinDays ?? 0;
|
|
168
|
+
if (n >= 0) {
|
|
169
|
+
return [{ gte: startOfUtcDay(today).toISOString(), lte: endOfUtcDay(addUtcDays(today, n)).toISOString() }];
|
|
170
|
+
}
|
|
171
|
+
return [{ gte: startOfUtcDay(addUtcDays(today, n)).toISOString(), lte: endOfUtcDay(today).toISOString() }];
|
|
172
|
+
}
|
|
173
|
+
function buildWindowWhere(desc, window) {
|
|
174
|
+
return {
|
|
175
|
+
...desc.filter ?? {},
|
|
176
|
+
[desc.dateField]: { $gte: window.gte, $lte: window.lte }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function errMessage(err) {
|
|
180
|
+
return err?.message ?? String(err);
|
|
181
|
+
}
|
|
182
|
+
var TimeRelativeTrigger = class {
|
|
183
|
+
constructor(getJobService, getDataEngine, logger, now = () => /* @__PURE__ */ new Date()) {
|
|
184
|
+
this.type = "time_relative";
|
|
185
|
+
/** flowName → job name registered for it, so stop() can cancel it. */
|
|
186
|
+
this.bound = /* @__PURE__ */ new Map();
|
|
187
|
+
this.getJobService = getJobService;
|
|
188
|
+
this.getDataEngine = getDataEngine;
|
|
189
|
+
this.logger = logger;
|
|
190
|
+
this.now = now;
|
|
191
|
+
}
|
|
192
|
+
start(binding, callback) {
|
|
193
|
+
const raw = binding.config?.timeRelative;
|
|
194
|
+
const parsed = TimeRelativeTriggerSchema.safeParse(raw);
|
|
195
|
+
if (!parsed.success) {
|
|
196
|
+
this.logger.warn(
|
|
197
|
+
`[time-relative] flow '${binding.flowName}' has no valid \`timeRelative\` descriptor \u2014 not bound. Provide { object, dateField, and exactly one of withinDays | offsetDays }. (${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")})`
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const desc = parsed.data;
|
|
202
|
+
const schedule = normalizeSchedule(binding.schedule) ?? { type: "cron", expression: TIME_RELATIVE_DEFAULT_CRON };
|
|
203
|
+
const jobService = this.getJobService();
|
|
204
|
+
if (!jobService || typeof jobService.schedule !== "function") {
|
|
205
|
+
this.logger.warn(
|
|
206
|
+
`[time-relative] job service unavailable \u2014 flow '${binding.flowName}' not scheduled`
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const engineNow = this.getDataEngine();
|
|
211
|
+
if (desc.object && engineNow && typeof engineNow.getObject === "function") {
|
|
212
|
+
let known;
|
|
213
|
+
try {
|
|
214
|
+
known = engineNow.getObject(desc.object);
|
|
215
|
+
} catch {
|
|
216
|
+
known = void 0;
|
|
217
|
+
}
|
|
218
|
+
if (!known) {
|
|
219
|
+
this.logger.warn(
|
|
220
|
+
`[time-relative] flow '${binding.flowName}' targets unknown object '${desc.object}' \u2014 the sweep is bound but will match nothing until that object is registered. Object names match exactly; check config.timeRelative.object.`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.stop(binding.flowName);
|
|
225
|
+
const jobName = `${JOB_PREFIX2}:${binding.flowName}`;
|
|
226
|
+
const maxRecords = desc.maxRecords ?? TIME_RELATIVE_DEFAULT_MAX_RECORDS;
|
|
227
|
+
const handler = async () => {
|
|
228
|
+
try {
|
|
229
|
+
await this.sweep(binding.flowName, desc, maxRecords, callback);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
this.logger.warn(
|
|
232
|
+
`[time-relative] flow '${binding.flowName}' sweep failed: ${errMessage(err)}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
this.bound.set(binding.flowName, jobName);
|
|
237
|
+
void Promise.resolve(jobService.schedule(jobName, schedule, handler)).then(() => {
|
|
238
|
+
const mode = desc.offsetDays ? `offsets [${desc.offsetDays.join(", ")}]d` : `within ${desc.withinDays}d`;
|
|
239
|
+
this.logger.info(
|
|
240
|
+
`[time-relative] bound flow '${binding.flowName}' \u2192 sweep '${desc.object}.${desc.dateField}' ${mode} on ${schedule.type}` + (schedule.expression ? ` '${schedule.expression}'` : "") + (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : "")
|
|
241
|
+
);
|
|
242
|
+
}).catch((err) => {
|
|
243
|
+
this.bound.delete(binding.flowName);
|
|
244
|
+
this.logger.warn(
|
|
245
|
+
`[time-relative] failed to schedule flow '${binding.flowName}': ${errMessage(err)}`
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Run one sweep: query each date window, union the matched records (deduped
|
|
251
|
+
* by id, capped at `maxRecords`), and launch the flow once per record. A
|
|
252
|
+
* per-record failure is isolated so one bad row never aborts the batch.
|
|
253
|
+
*/
|
|
254
|
+
async sweep(flowName, desc, maxRecords, callback) {
|
|
255
|
+
const engine = this.getDataEngine();
|
|
256
|
+
if (!engine || typeof engine.find !== "function") {
|
|
257
|
+
this.logger.warn(
|
|
258
|
+
`[time-relative] data engine unavailable \u2014 flow '${flowName}' sweep skipped this tick`
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const windows = computeDateWindows(desc, this.now());
|
|
263
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
264
|
+
const matched = [];
|
|
265
|
+
for (const window of windows) {
|
|
266
|
+
if (matched.length >= maxRecords) break;
|
|
267
|
+
const where = buildWindowWhere(desc, window);
|
|
268
|
+
const rows = await engine.find(desc.object, {
|
|
269
|
+
where,
|
|
270
|
+
limit: maxRecords,
|
|
271
|
+
context: { isSystem: true }
|
|
272
|
+
}) ?? [];
|
|
273
|
+
for (const row of rows) {
|
|
274
|
+
const id = row.id;
|
|
275
|
+
if (id != null) {
|
|
276
|
+
if (seenIds.has(id)) continue;
|
|
277
|
+
seenIds.add(id);
|
|
278
|
+
}
|
|
279
|
+
matched.push(row);
|
|
280
|
+
if (matched.length >= maxRecords) break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (matched.length >= maxRecords) {
|
|
284
|
+
this.logger.warn(
|
|
285
|
+
`[time-relative] flow '${flowName}' sweep hit the ${maxRecords}-record cap \u2014 some matching records were NOT processed this tick. Narrow the window/filter, or raise config.timeRelative.maxRecords.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
let launched = 0;
|
|
289
|
+
let failed = 0;
|
|
290
|
+
for (const record of matched) {
|
|
291
|
+
try {
|
|
292
|
+
const ctx = {
|
|
293
|
+
record,
|
|
294
|
+
object: desc.object,
|
|
295
|
+
event: "time_relative",
|
|
296
|
+
// Expose the record as params too, so flows with named `isInput`
|
|
297
|
+
// variables matching record fields get them seeded (parity with
|
|
298
|
+
// the record-change trigger).
|
|
299
|
+
params: record
|
|
300
|
+
};
|
|
301
|
+
await callback(ctx);
|
|
302
|
+
launched++;
|
|
303
|
+
} catch (err) {
|
|
304
|
+
failed++;
|
|
305
|
+
const log = this.logger.error?.bind(this.logger) ?? this.logger.warn.bind(this.logger);
|
|
306
|
+
log(
|
|
307
|
+
`[time-relative] flow '${flowName}' failed for record '${String(record.id ?? "?")}': ${errMessage(err)}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this.logger.debug?.(
|
|
312
|
+
`[time-relative] flow '${flowName}' swept '${desc.object}': ${matched.length} matched, ${launched} launched, ${failed} failed`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
stop(flowName) {
|
|
316
|
+
const jobName = this.bound.get(flowName);
|
|
317
|
+
if (!jobName) return;
|
|
318
|
+
this.bound.delete(flowName);
|
|
319
|
+
const jobService = this.getJobService();
|
|
320
|
+
if (!jobService || typeof jobService.cancel !== "function") return;
|
|
321
|
+
void Promise.resolve(jobService.cancel(jobName)).then(() => this.logger.debug?.(`[time-relative] unbound flow '${flowName}'`)).catch((err) => {
|
|
322
|
+
this.logger.warn(
|
|
323
|
+
`[time-relative] failed to unbind flow '${flowName}': ${errMessage(err)}`
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/time-relative-plugin.ts
|
|
330
|
+
var TimeRelativeTriggerPlugin = class {
|
|
331
|
+
constructor() {
|
|
332
|
+
this.name = "com.objectstack.trigger.time-relative";
|
|
333
|
+
this.type = "standard";
|
|
334
|
+
this.version = "1.0.0";
|
|
335
|
+
this.dependencies = ["com.objectstack.service.job", "com.objectstack.engine.objectql"];
|
|
336
|
+
}
|
|
337
|
+
async init(ctx) {
|
|
338
|
+
ctx.logger.info("Time-relative trigger plugin initialized");
|
|
339
|
+
}
|
|
340
|
+
async start(ctx) {
|
|
341
|
+
ctx.hook("kernel:ready", async () => {
|
|
342
|
+
const automation = this.resolveService(ctx, "automation");
|
|
343
|
+
if (!automation || typeof automation.registerTrigger !== "function") {
|
|
344
|
+
ctx.logger.warn(
|
|
345
|
+
"TimeRelativeTriggerPlugin: automation service not available \u2014 time-relative trigger NOT installed"
|
|
346
|
+
);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!this.resolveService(ctx, "job")) {
|
|
350
|
+
ctx.logger.warn(
|
|
351
|
+
"TimeRelativeTriggerPlugin: job service not available \u2014 time-relative sweeps will not run until one is registered"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (!this.resolveDataEngine(ctx)) {
|
|
355
|
+
ctx.logger.warn(
|
|
356
|
+
"TimeRelativeTriggerPlugin: ObjectQL engine not available \u2014 time-relative sweeps will find no records until it is"
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const trigger = new TimeRelativeTrigger(
|
|
360
|
+
() => this.resolveService(ctx, "job"),
|
|
361
|
+
() => this.resolveDataEngine(ctx),
|
|
362
|
+
ctx.logger
|
|
363
|
+
);
|
|
364
|
+
automation.registerTrigger(trigger);
|
|
365
|
+
ctx.logger.info("TimeRelativeTriggerPlugin: time-relative trigger registered");
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
resolveService(ctx, name) {
|
|
369
|
+
try {
|
|
370
|
+
return ctx.getService(name) ?? null;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
resolveDataEngine(ctx) {
|
|
376
|
+
return this.resolveService(ctx, "objectql") ?? this.resolveService(ctx, "data");
|
|
377
|
+
}
|
|
378
|
+
};
|
|
141
379
|
export {
|
|
142
380
|
ScheduleTrigger,
|
|
143
381
|
ScheduleTriggerPlugin,
|
|
382
|
+
TimeRelativeTrigger,
|
|
383
|
+
TimeRelativeTriggerPlugin,
|
|
384
|
+
buildWindowWhere,
|
|
385
|
+
computeDateWindows,
|
|
144
386
|
normalizeSchedule
|
|
145
387
|
};
|
|
146
388
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schedule-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AutomationContext } from '@objectstack/spec/contracts';\nimport type { JobSchedule, JobHandler } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding`\n * (service-automation/src/engine.ts). Declared locally so this trigger plugin\n * stays decoupled from the automation package — same pattern the record-change\n * trigger and the connector / messaging integrations use. The engine parses the\n * flow's start node and hands us a binding whose `schedule` carries the\n * cron/interval/once descriptor.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/**\n * Structural mirror of the engine's `FlowTrigger` extension point. The engine\n * calls {@link start} with a parsed binding + a callback that runs the flow,\n * and {@link stop} when the flow is unregistered/disabled.\n */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IJobService` this trigger needs: schedule a named job and\n * cancel it. Typed structurally so the plugin depends on the spec contract\n * shape, not a concrete adapter.\n */\nexport interface JobServiceSurface {\n schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;\n cancel(name: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst JOB_PREFIX = 'flow-schedule';\n\n/**\n * Normalize a flow's raw `schedule` descriptor into a {@link JobSchedule}, or\n * `null` if it can't be understood. Accepts the canonical\n * `{ type: 'cron'|'interval'|'once', ... }` shape plus a few ergonomic\n * shorthands (a bare cron string, `{ cron }`, `{ expression }`, `{ every }` /\n * `{ intervalMs }`, `{ at }`).\n */\nexport function normalizeSchedule(raw: unknown): JobSchedule | null {\n if (raw == null) return null;\n\n // Bare cron string, e.g. '0 1 * * *'.\n if (typeof raw === 'string') {\n const expr = raw.trim();\n return expr ? { type: 'cron', expression: expr } : null;\n }\n\n if (typeof raw !== 'object') return null;\n const s = raw as Record<string, unknown>;\n\n const type = typeof s.type === 'string' ? s.type : undefined;\n\n if (type === 'cron' || (!type && (typeof s.cron === 'string' || typeof s.expression === 'string'))) {\n const expression =\n (typeof s.expression === 'string' && s.expression) ||\n (typeof s.cron === 'string' && s.cron) ||\n undefined;\n if (!expression) return null;\n const out: JobSchedule = { type: 'cron', expression };\n if (typeof s.timezone === 'string') out.timezone = s.timezone;\n return out;\n }\n\n if (type === 'interval' || (!type && (typeof s.intervalMs === 'number' || typeof s.every === 'number'))) {\n const intervalMs =\n (typeof s.intervalMs === 'number' && s.intervalMs) ||\n (typeof s.every === 'number' && s.every) ||\n undefined;\n if (!intervalMs || intervalMs <= 0) return null;\n return { type: 'interval', intervalMs };\n }\n\n if (type === 'once' || (!type && typeof s.at === 'string')) {\n const at = typeof s.at === 'string' ? s.at : undefined;\n if (!at) return null;\n return { type: 'once', at };\n }\n\n return null;\n}\n\n/**\n * ScheduleTrigger\n *\n * Bridges the automation engine's {@link FlowTrigger} extension point to the\n * platform {@link JobServiceSurface}. For each schedule-triggered flow the\n * engine activates, it registers a job whose handler runs the flow; the job\n * service owns the actual cron/interval/once timing (so this trigger stays\n * adapter-agnostic — cron schedules need a cron-capable adapter, which the\n * job service selects).\n *\n * The job service is resolved lazily (per `start()`) via the supplied accessor,\n * so we always pick up the job service's *upgraded* adapter (e.g. the durable\n * DbJobAdapter that replaces the bootstrap interval adapter on `kernel:ready`).\n */\nexport class ScheduleTrigger implements FlowTrigger {\n readonly type = 'schedule';\n\n private readonly getJobService: () => JobServiceSurface | null;\n private readonly logger: TriggerLogger;\n /** flowName → job name registered for it, so stop() can cancel it. */\n private readonly bound = new Map<string, string>();\n\n constructor(getJobService: () => JobServiceSurface | null, logger: TriggerLogger) {\n this.getJobService = getJobService;\n this.logger = logger;\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const raw = binding.schedule ?? (binding.config as Record<string, unknown> | undefined)?.schedule;\n const schedule = normalizeSchedule(raw);\n if (!schedule) {\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' has no recognizable schedule descriptor — not bound`,\n );\n return;\n }\n\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.schedule !== 'function') {\n this.logger.warn(\n `[schedule] job service unavailable — flow '${binding.flowName}' not scheduled`,\n );\n return;\n }\n\n // Idempotent: drop any prior schedule for this flow before re-binding\n // (covers disable→enable cycles and hot reload).\n this.stop(binding.flowName);\n\n const jobName = `${JOB_PREFIX}:${binding.flowName}`;\n\n const handler: JobHandler = async ({ jobId }) => {\n try {\n const ctx: AutomationContext = {\n event: 'schedule',\n params: {\n jobId,\n flowName: binding.flowName,\n schedule,\n },\n };\n await callback(ctx);\n } catch (err) {\n // Error isolation: a scheduled flow failure must not crash the\n // job runner / ticker. Log and swallow.\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' execution failed: ${(err as Error)?.message ?? String(err)}`,\n );\n }\n };\n\n this.bound.set(binding.flowName, jobName);\n // FlowTrigger.start is sync; the job service's schedule() is async.\n // Fire-and-forget with error logging.\n void Promise.resolve(jobService.schedule(jobName, schedule, handler))\n .then(() => {\n this.logger.info(\n `[schedule] bound flow '${binding.flowName}' → ${schedule.type}` +\n (schedule.expression ? ` '${schedule.expression}'` : '') +\n (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : '') +\n (schedule.at ? ` at ${schedule.at}` : ''),\n );\n })\n .catch((err) => {\n this.bound.delete(binding.flowName);\n this.logger.warn(\n `[schedule] failed to schedule flow '${binding.flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n\n stop(flowName: string): void {\n const jobName = this.bound.get(flowName);\n if (!jobName) return;\n this.bound.delete(flowName);\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.cancel !== 'function') return;\n void Promise.resolve(jobService.cancel(jobName))\n .then(() => this.logger.debug?.(`[schedule] unbound flow '${flowName}'`))\n .catch((err) => {\n this.logger.warn(\n `[schedule] failed to unbind flow '${flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ScheduleTrigger } from './schedule-trigger.js';\nimport type { FlowTrigger, JobServiceSurface } from './schedule-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs: register a trigger on\n * its `FlowTrigger` extension point. Declared structurally so the plugin does\n * not take a build dependency on `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/**\n * ScheduleTriggerPlugin\n *\n * Makes schedule-triggered flows actually fire. The automation engine ships the\n * `FlowTrigger` wiring (it parses each flow's start node — `flow.type ===\n * 'schedule'` or a start-node `config.schedule` descriptor — into a binding and\n * calls `trigger.start(...)`), but the *concrete* schedule trigger lives here as\n * a plugin and delegates timing to the platform `IJobService` (the `'job'`\n * service). This mirrors the connector / record-change split (engine baseline +\n * trigger plugin).\n *\n * With this plugin (and a job service) installed, a flow whose start node\n * declares `config: { schedule: { type: 'cron', expression: '0 1 * * *' } }`\n * auto-launches on that schedule — no manual `engine.execute()`.\n *\n * Depends on the job service plugin so its `kernel:ready` upgrade (to the\n * durable DbJobAdapter) runs before ours; the job service is nonetheless\n * resolved lazily per `start()` so we always use its current adapter.\n */\nexport class ScheduleTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.schedule';\n type = 'standard';\n version = '7.3.0';\n dependencies = ['com.objectstack.service.job'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Schedule trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // The automation service + job service are resolvable once the kernel is\n // ready (kernel:ready fires after AutomationServicePlugin.start() has\n // pulled flows in and after the job service upgrades its adapter).\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: automation service not available — schedule trigger NOT installed',\n );\n return;\n }\n\n // Probe once for a clear startup warning; the trigger re-resolves\n // lazily on each start() so adapter upgrades are always picked up.\n if (!this.resolveService<JobServiceSurface>(ctx, 'job')) {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: job service not available — scheduled flows will not run until one is registered',\n );\n }\n\n const trigger = new ScheduleTrigger(\n () => this.resolveService<JobServiceSurface>(ctx, 'job'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n ctx.logger.info('ScheduleTriggerPlugin: schedule trigger registered');\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/schedule-trigger.ts","../src/plugin.ts","../src/time-relative-trigger.ts","../src/time-relative-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AutomationContext } from '@objectstack/spec/contracts';\nimport type { JobSchedule, JobHandler } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding`\n * (service-automation/src/engine.ts). Declared locally so this trigger plugin\n * stays decoupled from the automation package — same pattern the record-change\n * trigger and the connector / messaging integrations use. The engine parses the\n * flow's start node and hands us a binding whose `schedule` carries the\n * cron/interval/once descriptor.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/**\n * Structural mirror of the engine's `FlowTrigger` extension point. The engine\n * calls {@link start} with a parsed binding + a callback that runs the flow,\n * and {@link stop} when the flow is unregistered/disabled.\n */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IJobService` this trigger needs: schedule a named job and\n * cancel it. Typed structurally so the plugin depends on the spec contract\n * shape, not a concrete adapter.\n */\nexport interface JobServiceSurface {\n schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;\n cancel(name: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n /**\n * Execution failures log here when available (falling back to `warn`).\n * ERROR matters operationally: the CLI's boot-quiet window swallows\n * stdout (debug/info/warn) but stderr (error/fatal) always lands — so a\n * per-record sweep failure stays visible. Mirrors the record-change\n * trigger's logger surface.\n */\n error?(msg: string, ...args: unknown[]): void;\n}\n\nconst JOB_PREFIX = 'flow-schedule';\n\n/**\n * Normalize a flow's raw `schedule` descriptor into a {@link JobSchedule}, or\n * `null` if it can't be understood. Accepts the canonical\n * `{ type: 'cron'|'interval'|'once', ... }` shape plus a few ergonomic\n * shorthands (a bare cron string, `{ cron }`, `{ expression }`, `{ every }` /\n * `{ intervalMs }`, `{ at }`).\n */\nexport function normalizeSchedule(raw: unknown): JobSchedule | null {\n if (raw == null) return null;\n\n // Bare cron string, e.g. '0 1 * * *'.\n if (typeof raw === 'string') {\n const expr = raw.trim();\n return expr ? { type: 'cron', expression: expr } : null;\n }\n\n if (typeof raw !== 'object') return null;\n const s = raw as Record<string, unknown>;\n\n const type = typeof s.type === 'string' ? s.type : undefined;\n\n if (type === 'cron' || (!type && (typeof s.cron === 'string' || typeof s.expression === 'string'))) {\n const expression =\n (typeof s.expression === 'string' && s.expression) ||\n (typeof s.cron === 'string' && s.cron) ||\n undefined;\n if (!expression) return null;\n const out: JobSchedule = { type: 'cron', expression };\n if (typeof s.timezone === 'string') out.timezone = s.timezone;\n return out;\n }\n\n if (type === 'interval' || (!type && (typeof s.intervalMs === 'number' || typeof s.every === 'number'))) {\n const intervalMs =\n (typeof s.intervalMs === 'number' && s.intervalMs) ||\n (typeof s.every === 'number' && s.every) ||\n undefined;\n if (!intervalMs || intervalMs <= 0) return null;\n return { type: 'interval', intervalMs };\n }\n\n if (type === 'once' || (!type && typeof s.at === 'string')) {\n const at = typeof s.at === 'string' ? s.at : undefined;\n if (!at) return null;\n return { type: 'once', at };\n }\n\n return null;\n}\n\n/**\n * ScheduleTrigger\n *\n * Bridges the automation engine's {@link FlowTrigger} extension point to the\n * platform {@link JobServiceSurface}. For each schedule-triggered flow the\n * engine activates, it registers a job whose handler runs the flow; the job\n * service owns the actual cron/interval/once timing (so this trigger stays\n * adapter-agnostic — cron schedules need a cron-capable adapter, which the\n * job service selects).\n *\n * The job service is resolved lazily (per `start()`) via the supplied accessor,\n * so we always pick up the job service's *upgraded* adapter (e.g. the durable\n * DbJobAdapter that replaces the bootstrap interval adapter on `kernel:ready`).\n */\nexport class ScheduleTrigger implements FlowTrigger {\n readonly type = 'schedule';\n\n private readonly getJobService: () => JobServiceSurface | null;\n private readonly logger: TriggerLogger;\n /** flowName → job name registered for it, so stop() can cancel it. */\n private readonly bound = new Map<string, string>();\n\n constructor(getJobService: () => JobServiceSurface | null, logger: TriggerLogger) {\n this.getJobService = getJobService;\n this.logger = logger;\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const raw = binding.schedule ?? (binding.config as Record<string, unknown> | undefined)?.schedule;\n const schedule = normalizeSchedule(raw);\n if (!schedule) {\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' has no recognizable schedule descriptor — not bound`,\n );\n return;\n }\n\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.schedule !== 'function') {\n this.logger.warn(\n `[schedule] job service unavailable — flow '${binding.flowName}' not scheduled`,\n );\n return;\n }\n\n // Idempotent: drop any prior schedule for this flow before re-binding\n // (covers disable→enable cycles and hot reload).\n this.stop(binding.flowName);\n\n const jobName = `${JOB_PREFIX}:${binding.flowName}`;\n\n const handler: JobHandler = async ({ jobId }) => {\n try {\n const ctx: AutomationContext = {\n event: 'schedule',\n params: {\n jobId,\n flowName: binding.flowName,\n schedule,\n },\n };\n await callback(ctx);\n } catch (err) {\n // Error isolation: a scheduled flow failure must not crash the\n // job runner / ticker. Log and swallow.\n this.logger.warn(\n `[schedule] flow '${binding.flowName}' execution failed: ${(err as Error)?.message ?? String(err)}`,\n );\n }\n };\n\n this.bound.set(binding.flowName, jobName);\n // FlowTrigger.start is sync; the job service's schedule() is async.\n // Fire-and-forget with error logging.\n void Promise.resolve(jobService.schedule(jobName, schedule, handler))\n .then(() => {\n this.logger.info(\n `[schedule] bound flow '${binding.flowName}' → ${schedule.type}` +\n (schedule.expression ? ` '${schedule.expression}'` : '') +\n (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : '') +\n (schedule.at ? ` at ${schedule.at}` : ''),\n );\n })\n .catch((err) => {\n this.bound.delete(binding.flowName);\n this.logger.warn(\n `[schedule] failed to schedule flow '${binding.flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n\n stop(flowName: string): void {\n const jobName = this.bound.get(flowName);\n if (!jobName) return;\n this.bound.delete(flowName);\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.cancel !== 'function') return;\n void Promise.resolve(jobService.cancel(jobName))\n .then(() => this.logger.debug?.(`[schedule] unbound flow '${flowName}'`))\n .catch((err) => {\n this.logger.warn(\n `[schedule] failed to unbind flow '${flowName}': ${(err as Error)?.message ?? String(err)}`,\n );\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ScheduleTrigger } from './schedule-trigger.js';\nimport type { FlowTrigger, JobServiceSurface } from './schedule-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs: register a trigger on\n * its `FlowTrigger` extension point. Declared structurally so the plugin does\n * not take a build dependency on `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/**\n * ScheduleTriggerPlugin\n *\n * Makes schedule-triggered flows actually fire. The automation engine ships the\n * `FlowTrigger` wiring (it parses each flow's start node — `flow.type ===\n * 'schedule'` or a start-node `config.schedule` descriptor — into a binding and\n * calls `trigger.start(...)`), but the *concrete* schedule trigger lives here as\n * a plugin and delegates timing to the platform `IJobService` (the `'job'`\n * service). This mirrors the connector / record-change split (engine baseline +\n * trigger plugin).\n *\n * With this plugin (and a job service) installed, a flow whose start node\n * declares `config: { schedule: { type: 'cron', expression: '0 1 * * *' } }`\n * auto-launches on that schedule — no manual `engine.execute()`.\n *\n * Depends on the job service plugin so its `kernel:ready` upgrade (to the\n * durable DbJobAdapter) runs before ours; the job service is nonetheless\n * resolved lazily per `start()` so we always use its current adapter.\n */\nexport class ScheduleTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.schedule';\n type = 'standard';\n version = '7.3.0';\n dependencies = ['com.objectstack.service.job'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Schedule trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // The automation service + job service are resolvable once the kernel is\n // ready (kernel:ready fires after AutomationServicePlugin.start() has\n // pulled flows in and after the job service upgrades its adapter).\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: automation service not available — schedule trigger NOT installed',\n );\n return;\n }\n\n // Probe once for a clear startup warning; the trigger re-resolves\n // lazily on each start() so adapter upgrades are always picked up.\n if (!this.resolveService<JobServiceSurface>(ctx, 'job')) {\n ctx.logger.warn(\n 'ScheduleTriggerPlugin: job service not available — scheduled flows will not run until one is registered',\n );\n }\n\n const trigger = new ScheduleTrigger(\n () => this.resolveService<JobServiceSurface>(ctx, 'job'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n ctx.logger.info('ScheduleTriggerPlugin: schedule trigger registered');\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AutomationContext, JobSchedule, JobHandler } from '@objectstack/spec/contracts';\nimport {\n TimeRelativeTriggerSchema,\n TIME_RELATIVE_DEFAULT_CRON,\n TIME_RELATIVE_DEFAULT_MAX_RECORDS,\n} from '@objectstack/spec/automation';\nimport type { TimeRelativeTrigger as TimeRelativeDescriptor } from '@objectstack/spec/automation';\nimport { normalizeSchedule } from './schedule-trigger.js';\nimport type { FlowTrigger, FlowTriggerBinding, JobServiceSurface, TriggerLogger } from './schedule-trigger.js';\n\n/**\n * The slice of the ObjectQL data engine this trigger needs: run a filtered\n * `find` (to discover the records whose date field falls in the window) and,\n * optionally, probe whether an object is registered. Typed structurally — same\n * decoupling pattern the record-change trigger uses for its hook surface — so\n * this plugin does not take a build dependency on the engine package.\n */\nexport interface TimeRelativeDataEngine {\n find(\n objectName: string,\n query?: {\n where?: Record<string, unknown>;\n fields?: string[];\n limit?: number;\n /** Elevated context — a background sweep must see all rows, not RLS-scoped ones. */\n context?: { isSystem?: boolean };\n },\n ): Promise<Array<Record<string, unknown>> | undefined>;\n /**\n * Optional object-existence probe (the ObjectQL engine's `getObject`).\n * When present, {@link TimeRelativeTrigger.start} uses it to call out a\n * descriptor whose `object` matches no registered object at bind time —\n * otherwise the sweep just quietly finds nothing forever.\n */\n getObject?(name: string): unknown;\n}\n\n/** Job-name namespace so time-relative sweeps never collide with plain schedule jobs. */\nconst JOB_PREFIX = 'flow-time-relative';\n\nconst MS_PER_DAY = 86_400_000;\n\n/** A closed, inclusive instant window `[gte, lte]` as ISO-8601 strings. */\nexport interface DateWindow {\n /** Lower bound (inclusive), ISO-8601. */\n gte: string;\n /** Upper bound (inclusive), ISO-8601. */\n lte: string;\n}\n\n// ─── Pure window math (day-granular, UTC) ───────────────────────────\n\n/** Start of `d`'s UTC calendar day (00:00:00.000Z). */\nfunction startOfUtcDay(d: Date): Date {\n return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));\n}\n\n/** End of `d`'s UTC calendar day (23:59:59.999Z) — inclusive upper bound. */\nfunction endOfUtcDay(d: Date): Date {\n return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 23, 59, 59, 999));\n}\n\n/** `d`'s UTC day shifted by `n` whole days (exact in UTC — no DST drift). */\nfunction addUtcDays(d: Date, n: number): Date {\n return new Date(startOfUtcDay(d).getTime() + n * MS_PER_DAY);\n}\n\n/**\n * Compute the inclusive date window(s) a descriptor selects, relative to `now`.\n *\n * - `offsetDays` → one single-day window per offset (`today + offset`), so the\n * sweep fires exactly on each threshold day (the robust T-minus reminder).\n * - `withinDays` → one range window: `[today, today + N]` when N ≥ 0 (upcoming),\n * or `[today − |N|, today]` when N < 0 (overdue lookback). Always includes today.\n *\n * Day-granular and computed in UTC. The upper bound is the *end* of its day\n * (`23:59:59.999Z`), so a `datetime` field matches for the whole day and a\n * `date` field (compared as `YYYY-MM-DD` after the driver truncates) is inclusive.\n */\nexport function computeDateWindows(desc: TimeRelativeDescriptor, now: Date): DateWindow[] {\n const today = startOfUtcDay(now);\n\n if (desc.offsetDays && desc.offsetDays.length > 0) {\n return desc.offsetDays.map((offset) => {\n const day = addUtcDays(today, offset);\n return { gte: startOfUtcDay(day).toISOString(), lte: endOfUtcDay(day).toISOString() };\n });\n }\n\n const n = desc.withinDays ?? 0;\n if (n >= 0) {\n return [{ gte: startOfUtcDay(today).toISOString(), lte: endOfUtcDay(addUtcDays(today, n)).toISOString() }];\n }\n // Negative: window extends into the past, still anchored to (and including) today.\n return [{ gte: startOfUtcDay(addUtcDays(today, n)).toISOString(), lte: endOfUtcDay(today).toISOString() }];\n}\n\n/**\n * Build the ObjectQL `where` map for one date window: the descriptor's static\n * `filter` (if any) ANDed with a `$gte`/`$lte` range on the date field. The map\n * form is the canonical filter shape both drivers evaluate verbatim (the same\n * shape the platform's own retention sweep uses).\n */\nexport function buildWindowWhere(desc: TimeRelativeDescriptor, window: DateWindow): Record<string, unknown> {\n return {\n ...(desc.filter ?? {}),\n [desc.dateField]: { $gte: window.gte, $lte: window.lte },\n };\n}\n\nfunction errMessage(err: unknown): string {\n return (err as Error)?.message ?? String(err);\n}\n\n/**\n * TimeRelativeTrigger\n *\n * The declarative answer to \"act on records whose date field is coming up (or\n * overdue)\" (#1874). Instead of the fragile date-equality-on-record-change\n * pattern (which only fires if the record happens to be edited on the threshold\n * day) or a hand-rolled cron + range query per flow, a flow whose start node\n * declares `config.timeRelative` is swept on a schedule (daily by default) and\n * launched **once per matching record**.\n *\n * It composes the schedule trigger's two collaborators:\n * - the platform {@link JobServiceSurface} owns the sweep cadence (like the\n * plain schedule trigger), and\n * - the {@link TimeRelativeDataEngine} runs the date-window query (like the\n * record-change trigger reaching ObjectQL).\n *\n * Both are resolved lazily (per call) so adapter upgrades — the durable job\n * adapter that replaces the bootstrap ticker on `kernel:ready`, a late-registered\n * data engine — are always picked up. The engine owns the start-node `condition`\n * gate and `runAs` identity, so this trigger only has to put the matched record\n * on the {@link AutomationContext}; `{record.<field>}` interpolation and the\n * condition work exactly as they do for a record-change flow.\n */\nexport class TimeRelativeTrigger implements FlowTrigger {\n readonly type = 'time_relative';\n\n private readonly getJobService: () => JobServiceSurface | null;\n private readonly getDataEngine: () => TimeRelativeDataEngine | null;\n private readonly logger: TriggerLogger;\n /** Injectable clock so window math is deterministic under test. */\n private readonly now: () => Date;\n /** flowName → job name registered for it, so stop() can cancel it. */\n private readonly bound = new Map<string, string>();\n\n constructor(\n getJobService: () => JobServiceSurface | null,\n getDataEngine: () => TimeRelativeDataEngine | null,\n logger: TriggerLogger,\n now: () => Date = () => new Date(),\n ) {\n this.getJobService = getJobService;\n this.getDataEngine = getDataEngine;\n this.logger = logger;\n this.now = now;\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const raw = (binding.config as Record<string, unknown> | undefined)?.timeRelative;\n const parsed = TimeRelativeTriggerSchema.safeParse(raw);\n if (!parsed.success) {\n this.logger.warn(\n `[time-relative] flow '${binding.flowName}' has no valid \\`timeRelative\\` descriptor — not bound. ` +\n `Provide { object, dateField, and exactly one of withinDays | offsetDays }. ` +\n `(${parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ')})`,\n );\n return;\n }\n const desc = parsed.data;\n\n // Cadence: the flow's start-node schedule descriptor, or a daily default.\n // A daily sweep is the whole point (evaluate the window every day so a\n // threshold day is never missed), so an omitted schedule means \"daily\",\n // not \"never\".\n const schedule: JobSchedule =\n normalizeSchedule(binding.schedule) ?? { type: 'cron', expression: TIME_RELATIVE_DEFAULT_CRON };\n\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.schedule !== 'function') {\n this.logger.warn(\n `[time-relative] job service unavailable — flow '${binding.flowName}' not scheduled`,\n );\n return;\n }\n\n // Best-effort object-existence probe at bind time (the engine may be\n // available now even though the sweep resolves it lazily). A descriptor\n // targeting an unknown object would sweep forever finding nothing.\n const engineNow = this.getDataEngine();\n if (desc.object && engineNow && typeof engineNow.getObject === 'function') {\n let known: unknown;\n try {\n known = engineNow.getObject(desc.object);\n } catch {\n known = undefined;\n }\n if (!known) {\n this.logger.warn(\n `[time-relative] flow '${binding.flowName}' targets unknown object '${desc.object}' — the sweep is bound but will match nothing until that object is registered. ` +\n `Object names match exactly; check config.timeRelative.object.`,\n );\n }\n }\n\n // Idempotent: drop any prior schedule for this flow before re-binding\n // (covers disable→enable cycles and hot reload).\n this.stop(binding.flowName);\n\n const jobName = `${JOB_PREFIX}:${binding.flowName}`;\n const maxRecords = desc.maxRecords ?? TIME_RELATIVE_DEFAULT_MAX_RECORDS;\n\n const handler: JobHandler = async () => {\n try {\n await this.sweep(binding.flowName, desc, maxRecords, callback);\n } catch (err) {\n // Error isolation: a sweep failure must not crash the job\n // runner / ticker. Log and swallow.\n this.logger.warn(\n `[time-relative] flow '${binding.flowName}' sweep failed: ${errMessage(err)}`,\n );\n }\n };\n\n this.bound.set(binding.flowName, jobName);\n // FlowTrigger.start is sync; the job service's schedule() is async.\n // Fire-and-forget with error logging (mirrors ScheduleTrigger).\n void Promise.resolve(jobService.schedule(jobName, schedule, handler))\n .then(() => {\n const mode = desc.offsetDays\n ? `offsets [${desc.offsetDays.join(', ')}]d`\n : `within ${desc.withinDays}d`;\n this.logger.info(\n `[time-relative] bound flow '${binding.flowName}' → sweep '${desc.object}.${desc.dateField}' ${mode} on ${schedule.type}` +\n (schedule.expression ? ` '${schedule.expression}'` : '') +\n (schedule.intervalMs ? ` every ${schedule.intervalMs}ms` : ''),\n );\n })\n .catch((err) => {\n this.bound.delete(binding.flowName);\n this.logger.warn(\n `[time-relative] failed to schedule flow '${binding.flowName}': ${errMessage(err)}`,\n );\n });\n }\n\n /**\n * Run one sweep: query each date window, union the matched records (deduped\n * by id, capped at `maxRecords`), and launch the flow once per record. A\n * per-record failure is isolated so one bad row never aborts the batch.\n */\n private async sweep(\n flowName: string,\n desc: TimeRelativeDescriptor,\n maxRecords: number,\n callback: (ctx: AutomationContext) => Promise<void>,\n ): Promise<void> {\n const engine = this.getDataEngine();\n if (!engine || typeof engine.find !== 'function') {\n this.logger.warn(\n `[time-relative] data engine unavailable — flow '${flowName}' sweep skipped this tick`,\n );\n return;\n }\n\n const windows = computeDateWindows(desc, this.now());\n const seenIds = new Set<unknown>();\n const matched: Array<Record<string, unknown>> = [];\n\n for (const window of windows) {\n if (matched.length >= maxRecords) break;\n const where = buildWindowWhere(desc, window);\n const rows =\n (await engine.find(desc.object, {\n where,\n limit: maxRecords,\n context: { isSystem: true },\n })) ?? [];\n for (const row of rows) {\n const id = (row as { id?: unknown }).id;\n // Dedup across windows (offset mode) by id; rows without an id\n // are always kept (can't dedup, better than dropping).\n if (id != null) {\n if (seenIds.has(id)) continue;\n seenIds.add(id);\n }\n matched.push(row);\n if (matched.length >= maxRecords) break;\n }\n }\n\n if (matched.length >= maxRecords) {\n this.logger.warn(\n `[time-relative] flow '${flowName}' sweep hit the ${maxRecords}-record cap — some matching records were NOT processed this tick. ` +\n `Narrow the window/filter, or raise config.timeRelative.maxRecords.`,\n );\n }\n\n let launched = 0;\n let failed = 0;\n for (const record of matched) {\n try {\n const ctx: AutomationContext = {\n record,\n object: desc.object,\n event: 'time_relative',\n // Expose the record as params too, so flows with named `isInput`\n // variables matching record fields get them seeded (parity with\n // the record-change trigger).\n params: record,\n };\n await callback(ctx);\n launched++;\n } catch (err) {\n failed++;\n // Error isolation per record: one failing flow run must not stop\n // the sweep. ERROR when available (stderr survives the CLI's\n // boot-quiet stdout window), else warn.\n const log = this.logger.error?.bind(this.logger) ?? this.logger.warn.bind(this.logger);\n log(\n `[time-relative] flow '${flowName}' failed for record '${String((record as { id?: unknown }).id ?? '?')}': ${errMessage(err)}`,\n );\n }\n }\n\n this.logger.debug?.(\n `[time-relative] flow '${flowName}' swept '${desc.object}': ${matched.length} matched, ${launched} launched, ${failed} failed`,\n );\n }\n\n stop(flowName: string): void {\n const jobName = this.bound.get(flowName);\n if (!jobName) return;\n this.bound.delete(flowName);\n const jobService = this.getJobService();\n if (!jobService || typeof jobService.cancel !== 'function') return;\n void Promise.resolve(jobService.cancel(jobName))\n .then(() => this.logger.debug?.(`[time-relative] unbound flow '${flowName}'`))\n .catch((err) => {\n this.logger.warn(\n `[time-relative] failed to unbind flow '${flowName}': ${errMessage(err)}`,\n );\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { TimeRelativeTrigger } from './time-relative-trigger.js';\nimport type { TimeRelativeDataEngine } from './time-relative-trigger.js';\nimport type { FlowTrigger, JobServiceSurface } from './schedule-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs: register a trigger on\n * its `FlowTrigger` extension point. Declared structurally so the plugin does\n * not take a build dependency on `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/**\n * TimeRelativeTriggerPlugin\n *\n * Arms **declarative time-relative flows** (#1874): a flow whose start node\n * declares `config.timeRelative` (object + dateField + `withinDays`/`offsetDays`)\n * is swept on a schedule and launched once per record whose date field falls in\n * the window — no hand-written cron + range query, no fragile\n * date-equality-on-record-change.\n *\n * It ships in `@objectstack/trigger-schedule` alongside the plain schedule\n * trigger (both are schedule-driven) but is a **separate** plugin: the\n * time-relative trigger additionally needs the ObjectQL engine (for the sweep\n * query), so keeping it separate leaves the plain `ScheduleTriggerPlugin`'s\n * dependency surface unchanged. Depends on the job service (sweep cadence) and\n * the ObjectQL engine (record discovery); both are resolved lazily per `start()`\n * so adapter upgrades are always picked up.\n */\nexport class TimeRelativeTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.time-relative';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.service.job', 'com.objectstack.engine.objectql'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Time-relative trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // The automation service, job service, and ObjectQL engine are all\n // resolvable once the kernel is ready (kernel:ready fires after\n // AutomationServicePlugin.start() has pulled flows in and after the job\n // service upgrades its adapter).\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn(\n 'TimeRelativeTriggerPlugin: automation service not available — time-relative trigger NOT installed',\n );\n return;\n }\n\n // Probe once for a clear startup warning; the trigger re-resolves\n // both collaborators lazily on each start()/sweep so late upgrades\n // are always picked up.\n if (!this.resolveService<JobServiceSurface>(ctx, 'job')) {\n ctx.logger.warn(\n 'TimeRelativeTriggerPlugin: job service not available — time-relative sweeps will not run until one is registered',\n );\n }\n if (!this.resolveDataEngine(ctx)) {\n ctx.logger.warn(\n 'TimeRelativeTriggerPlugin: ObjectQL engine not available — time-relative sweeps will find no records until it is',\n );\n }\n\n const trigger = new TimeRelativeTrigger(\n () => this.resolveService<JobServiceSurface>(ctx, 'job'),\n () => this.resolveDataEngine(ctx),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n ctx.logger.info('TimeRelativeTriggerPlugin: time-relative trigger registered');\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n\n private resolveDataEngine(ctx: PluginContext): TimeRelativeDataEngine | null {\n // Primary alias 'objectql', fallback 'data' (some kernels register the\n // engine under both) — same lookup the record-change trigger uses.\n return (\n this.resolveService<TimeRelativeDataEngine>(ctx, 'objectql') ??\n this.resolveService<TimeRelativeDataEngine>(ctx, 'data')\n );\n }\n}\n"],"mappings":"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","names":["JOB_PREFIX"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/trigger-schedule",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0-rc.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Schedule flow trigger for ObjectStack — auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "
|
|
17
|
-
"@objectstack/spec": "
|
|
16
|
+
"@objectstack/core": "16.0.0-rc.0",
|
|
17
|
+
"@objectstack/spec": "16.0.0-rc.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^26.1.1",
|
|
21
21
|
"typescript": "^6.0.3",
|
|
22
22
|
"vitest": "^4.1.10",
|
|
23
|
-
"@objectstack/service-automation": "
|
|
23
|
+
"@objectstack/service-automation": "16.0.0-rc.0"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"objectstack",
|