@amerilux/netsuite-api 0.4.0 → 0.5.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 +81 -5
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.ts +137 -0
- package/dist/server/apiError.d.ts +2 -0
- package/dist/server/apiError.js +4 -0
- package/dist/server/defineJob.d.ts +95 -0
- package/dist/server/defineJob.js +150 -0
- package/dist/server/index.d.ts +8 -3
- package/dist/server/index.js +5 -2
- package/dist/server/jobRuns.d.ts +65 -0
- package/dist/server/jobRuns.js +292 -0
- package/dist/testing/N/task.d.ts +14 -0
- package/dist/testing/N/task.js +4 -1
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/jobs.d.ts +50 -0
- package/dist/testing/jobs.js +90 -0
- package/dist-tooling/cli/main.js +6 -3
- package/dist-tooling/config.d.ts +33 -0
- package/dist-tooling/config.js +84 -2
- package/dist-tooling/controllerReader.d.ts +22 -0
- package/dist-tooling/controllerReader.js +5 -5
- package/dist-tooling/emit.d.ts +42 -0
- package/dist-tooling/emit.js +126 -3
- package/dist-tooling/generate.d.ts +10 -1
- package/dist-tooling/generate.js +115 -25
- package/dist-tooling/index.d.ts +7 -4
- package/dist-tooling/index.js +3 -1
- package/dist-tooling/jobReader.d.ts +53 -0
- package/dist-tooling/jobReader.js +260 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The API layer for a NetSuite single-page app. The app's server side is SuiteScript; its browser side is a bundle served by a Suitelet. This package holds what sits between them, so a project writes controllers and services and nothing else:
|
|
4
4
|
|
|
5
|
-
- **`@amerilux/netsuite-api/server`**: declare a controller's endpoints and the script that serves them, expose them as a Restlet or a Suitelet, reject a call with an `ApiError`, call another Suitelet controller from server code, and find a File Cabinet file by name.
|
|
5
|
+
- **`@amerilux/netsuite-api/server`**: declare a controller's endpoints and the script that serves them, expose them as a Restlet or a Suitelet, reject a call with an `ApiError`, call another Suitelet controller from server code, write a Map/Reduce job as stages and start one, and find a File Cabinet file by name.
|
|
6
6
|
- **`@amerilux/netsuite-api/client`**: a typed browser client per controller, built from the endpoint types.
|
|
7
7
|
- **`@amerilux/netsuite-api/testing`**: stubs for the `N/*` modules and the vitest wiring that routes imports to them.
|
|
8
|
-
- **`netsuite-api generate`**: reads the controllers and writes the client's whole view of the backend, one module
|
|
9
|
-
- **`@amerilux/netsuite-api`** (the root): the wire itself. The envelope, the endpoint types, `ScriptDeclaration`, `ScriptRef`.
|
|
8
|
+
- **`netsuite-api generate`**: reads the controllers and the jobs and writes the client's whole view of the backend, one module each and an index re-exporting them, plus the server-side map of scripts and jobs. The client never imports from the server tree.
|
|
9
|
+
- **`@amerilux/netsuite-api`** (the root): the wire itself. The envelope, the endpoint types, `ScriptDeclaration`, `ScriptRef`, `JobRef`, `JobRun`.
|
|
10
10
|
|
|
11
11
|
The layout it assumes is the one `create-netsuite-project` scaffolds: `api/` (SuiteScript) and `client/` (React) as workspaces.
|
|
12
12
|
|
|
@@ -115,6 +115,7 @@ A hook imports `{ customer }` from it, calls `customer.api.search({ search: 'acm
|
|
|
115
115
|
```json
|
|
116
116
|
{
|
|
117
117
|
"controllers": "api/src/controllers",
|
|
118
|
+
"jobs": "api/src/jobs",
|
|
118
119
|
"outDir": "client/src/api",
|
|
119
120
|
"scriptsOutFile": "api/src/scripts.gen.ts",
|
|
120
121
|
"clientModule": "@amerilux/netsuite-api/client",
|
|
@@ -124,7 +125,7 @@ A hook imports `{ customer }` from it, calls `customer.api.search({ search: 'acm
|
|
|
124
125
|
}
|
|
125
126
|
```
|
|
126
127
|
|
|
127
|
-
Paths are relative to the config file. `outDir` holds the controller modules and
|
|
128
|
+
Paths are relative to the config file. `outDir` holds the controller and job modules and their indexes, and nothing else. A project with jobs adds a `jobRuns` block naming the run record it deployed; see **Jobs**. `inlineTypes` maps a specifier as written in a controller to the file whose type declarations are copied into the module of every controller importing from it; a key with one `*` stands for a file name and the `*` in its file takes that name, so `../services/*` covers every service. Only the type declarations of a file are read, so a service's functions are skipped; a type in one inlined file that refers to a type imported from another (a service's summary type built on an entity type) brings that type along, the import resolved through the same map as written from the same folder depth. `typeImports` maps a specifier to the one the client resolves, for a type that stays an import (the package's server entry maps to its client entry so `RawResponse` carries over). A type imported from any other module is an error.
|
|
128
129
|
|
|
129
130
|
## The client at runtime
|
|
130
131
|
|
|
@@ -192,9 +193,84 @@ const userRolesApi = createSuiteletClient<UserRolesEndpoints>(scripts.userRoles)
|
|
|
192
193
|
export const listRolesForEmployee = (employeeId: number) => userRolesApi.byEmployee({ employeeId }).roles;
|
|
193
194
|
```
|
|
194
195
|
|
|
196
|
+
## Jobs
|
|
197
|
+
|
|
198
|
+
A job is a Map/Reduce script written as stages. What the wrapper adds is the run: a Map/Reduce answers nothing and cannot be waited on, so every run is a row in a record of the application's own, and that row is what server code and the browser talk about.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
/**
|
|
202
|
+
* @NApiVersion 2.1
|
|
203
|
+
* @NScriptType MapReduceScript
|
|
204
|
+
*/
|
|
205
|
+
import { defineJob } from '@amerilux/netsuite-api/server';
|
|
206
|
+
import { jobRuns } from '../scripts.gen';
|
|
207
|
+
import { closeOrder, listStaleOrders, type StaleOrder } from '../services/staleOrderService';
|
|
208
|
+
|
|
209
|
+
export interface CloseStaleRequest { olderThanDays: number }
|
|
210
|
+
export interface CloseStaleResult { closed: number }
|
|
211
|
+
|
|
212
|
+
export const { getInputData, map, summarize } = defineJob({
|
|
213
|
+
name: 'closeStaleOrders',
|
|
214
|
+
scriptId: 'customscript_app_close_stale_mr',
|
|
215
|
+
deployments: ['customdeploy_app_close_stale_mr', 'customdeploy_app_close_stale_mr_2'],
|
|
216
|
+
runParameter: 'custscript_app_close_stale_run',
|
|
217
|
+
parameters: { batchSize: { id: 'custscript_app_close_stale_batch', type: 'integer' } },
|
|
218
|
+
runs: jobRuns,
|
|
219
|
+
}, {
|
|
220
|
+
getInputData: (input: CloseStaleRequest): StaleOrder[] => listStaleOrders(input.olderThanDays),
|
|
221
|
+
map: (order: StaleOrder, job): void => {
|
|
222
|
+
if (closeOrder(order.id)) job.write(String(order.id), order.id);
|
|
223
|
+
},
|
|
224
|
+
summarize: (summary): CloseStaleResult => ({ closed: summary.output.length }),
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The first parameter of `getInputData` is the run's input and the return type of `summarize` is its result: the generator reads the shapes from those two annotations, the way it reads an endpoint's request and response. The values carried between stages are JSON, so each stage annotates what it expects (`values: number[]` on a reduce stage, `summary: JobSummary<Total>` on summarize) and the wrapper hands them back that way. Export the stages the job has, and `summarize` always: the run is closed there. A stage exported without being declared throws when NetSuite calls it, rather than quietly passing values through.
|
|
229
|
+
|
|
230
|
+
Plenty of jobs answer nothing, because the records they write are the point. Such a job declares no `summarize` and still exports it, and the wrapper closes the run for it; one that wants a last word without a result (a notification when the run ends) declares `summarize` with a `void` return. Either way the run's `Result` is `null`, and a page watches `status`, the progress fields and `errors` instead of a result.
|
|
231
|
+
|
|
232
|
+
Reading a run asks the task about progress as well. `stagePercentComplete` is `getPercentageCompleted()`, which NetSuite documents as the percentage complete of the **stage being processed**, so it counts to 100 once per stage; `itemsProcessed` and `itemsTotal` come from that stage's `getTotal*Count()` and `getPending*Count()` pair and only go up. The record keeps the percent (100 once a run ends) but never the counts, so the counts are null for a run that has ended or whose task id NetSuite has purged — by then the run has the result, which the record did keep.
|
|
233
|
+
|
|
234
|
+
The store belongs in a repository, because it writes a record and submits a task. Keep it job-agnostic: whatever decides a run should start (a service, in the layout the template scaffolds) passes the job and the input.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// api/src/repositories/jobRunRepository.ts
|
|
238
|
+
import { createJobRunStore } from '@amerilux/netsuite-api/server';
|
|
239
|
+
import type { JobRef, JobRun } from '@amerilux/netsuite-api/server';
|
|
240
|
+
import { jobRuns } from '../scripts.gen';
|
|
241
|
+
|
|
242
|
+
const jobRunStore = createJobRunStore(jobRuns);
|
|
243
|
+
|
|
244
|
+
export const startJobRun = (job: JobRef, input: unknown): string => jobRunStore.start(job, input);
|
|
245
|
+
export const findJobRun = (runId: string): JobRun | null => jobRunStore.read(runId);
|
|
246
|
+
|
|
247
|
+
// api/src/services/ordersService.ts
|
|
248
|
+
export const startClosingOldOrders = (olderThanDays: number) => startJobRun(jobs.closeOldOrders, { olderThanDays } satisfies CloseOldOrdersRequest);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`start` writes the run, then submits the task to the first deployment that takes it; NetSuite runs one instance of a deployment at a time, so the list in the declaration is how many runs can overlap. When they are all running it throws `ApiError.conflict` (409) and removes the run it had written, because nothing started. A controller endpoint hands the run id to the browser, which polls another endpoint for `read`.
|
|
252
|
+
|
|
253
|
+
`read` is the reason a dead run does not look like a working one: it takes status, stage and progress from `N/task.checkStatus` as well as the record, so a task NetSuite gave up on is `failed`, and a task that finished without writing a result is `failed` too. `findExpired(days)` and `remove` are what a cleanup job runs on a schedule; run records are not meant to be permanent.
|
|
254
|
+
|
|
255
|
+
`findRuns({ job, startedBy, unfinishedOnly, limit })` answers the matching runs newest first, in one query and without loading a record: it is how a page finds the run it lost track of, because the run knows who started it even after a browser has forgotten. Its rows carry what the record says, not what the task says, so read a run by id before believing one is still working — and they carry no progress at all, because only the task has any.
|
|
256
|
+
|
|
257
|
+
### The run record
|
|
258
|
+
|
|
259
|
+
The shape is this package's and the ids are the application's, because the record carries the application's prefix. `netsuite-api.config.json` names them and the generator writes them into the scripts map as `jobRuns`:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
"jobRuns": {
|
|
263
|
+
"recordType": "customrecord_app_job_run",
|
|
264
|
+
"fieldPrefix": "custrecord_app_jr",
|
|
265
|
+
"extraFields": { "customerId": { "id": "custrecord_app_jr_customer", "type": "integer" } }
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Each field's id is the prefix plus a short suffix (`_job`, `_status`, `_stage`, `_percent`, `_input`, `_result`, `_errors`, `_task`, `_deploy`, `_by`, `_start`, `_end`); `fields` overrides any one of them for a record whose field is named differently. `extraFields` are the fields the application added: `start` sets them (`{ extra: { customerId } }`) and a run reports them back under `extra`, typed.
|
|
270
|
+
|
|
195
271
|
## Tests
|
|
196
272
|
|
|
197
|
-
`N/*` modules exist only inside NetSuite. The `testing` entry ships a stub per module, each export a `vi.fn()`, and the vitest wiring:
|
|
273
|
+
`N/*` modules exist only inside NetSuite. The `testing` entry ships a stub per module, each export a `vi.fn()`, the Map/Reduce contexts a job's stages are called with (`mapContextFor`, `reduceContextFor`, `summarizeContextFor`, each capturing what the stage wrote), and the vitest wiring:
|
|
198
274
|
|
|
199
275
|
```ts
|
|
200
276
|
import { inlinedPackagesForNetsuiteStubs, netsuiteModuleStubAliases } from '@amerilux/netsuite-api/testing';
|
package/dist/client/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
export { ApiClientError, NETSUITE_API_BASE_PATHS, NO_RESPONSE_STATUS, buildApiUrl, callEndpoint, callRawEndpoint, configureApiClient, createApiClient } from './apiClient.js';
|
|
6
6
|
export type { ApiBasePaths, ApiCallContext, ApiCallOptions, ApiClient, ApiClientConfiguration, ApiClientOptions, ApiErrorHandler, ClientResponse } from './apiClient.js';
|
|
7
7
|
export { ENDPOINT_PARAMETER } from '../index.js';
|
|
8
|
-
export type { ApiEnvelope, ApiErrorBody, Endpoint, Endpoints, EndpointRequest, EndpointResponse, RawResponse, ScriptDeclaration, ScriptKind, ScriptRef } from '../index.js';
|
|
8
|
+
export type { ApiEnvelope, ApiErrorBody, Endpoint, Endpoints, EndpointRequest, EndpointResponse, JobRun, JobRunError, JobRunListEntry, JobRunQuery, JobRunStage, JobRunStatus, RawResponse, ScriptDeclaration, ScriptKind, ScriptRef, } from '../index.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -79,3 +79,140 @@ export interface ScriptRef {
|
|
|
79
79
|
deployId: string;
|
|
80
80
|
browser?: boolean;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Jobs: the Map/Reduce side of the wire. A job answers nothing, so its run is the thing both sides
|
|
84
|
+
* talk about: server code starts one and gets a run id, and the browser asks what that run is doing.
|
|
85
|
+
* A run lives in a record of the application's own (`customrecord_<prefix>_job_run`, whose ids the
|
|
86
|
+
* generator reads from netsuite-api.config.json), so a job that dies before it writes anything is
|
|
87
|
+
* still a run that failed, not a page waiting forever.
|
|
88
|
+
*/
|
|
89
|
+
/** How NetSuite stores a script parameter or a run field, and the type the stages and the page see. */
|
|
90
|
+
export type NetsuiteValueType = 'text' | 'integer' | 'decimal' | 'checkbox' | 'date' | 'select';
|
|
91
|
+
/** One script parameter of a job: its id in NetSuite and what NetSuite stores in it. */
|
|
92
|
+
export interface JobParameterDeclaration {
|
|
93
|
+
id: string;
|
|
94
|
+
type: NetsuiteValueType;
|
|
95
|
+
}
|
|
96
|
+
/** The value of one parameter as the stages see it. A date arrives as the ISO string, never a Date: a stage is not the wire, but a run record is read by both sides. */
|
|
97
|
+
export type JobParameterValue<TType extends NetsuiteValueType> = TType extends 'integer' | 'decimal' ? number : TType extends 'checkbox' ? boolean : string;
|
|
98
|
+
/** Every parameter a job declares, by the name the stages use. */
|
|
99
|
+
export type JobParameterValues<TParameters extends Record<string, JobParameterDeclaration>> = {
|
|
100
|
+
readonly [TName in keyof TParameters]: JobParameterValue<TParameters[TName]['type']>;
|
|
101
|
+
};
|
|
102
|
+
/** Where a run is: NetSuite's own stage names, as the run record and the page speak them. */
|
|
103
|
+
export type JobRunStage = 'input' | 'map' | 'shuffle' | 'reduce' | 'summarize';
|
|
104
|
+
/**
|
|
105
|
+
* A run's state. `pending` is submitted but not started, `running` is anything between, and
|
|
106
|
+
* `complete` means summarize wrote the result. `failed` is either a stage that threw or a task
|
|
107
|
+
* NetSuite gave up on, which is why a run is read through checkStatus as well as its record.
|
|
108
|
+
*/
|
|
109
|
+
export type JobRunStatus = 'pending' | 'running' | 'complete' | 'failed';
|
|
110
|
+
/** One thing that went wrong in a run: a stage's own throw, or a key NetSuite could not finish. */
|
|
111
|
+
export interface JobRunError {
|
|
112
|
+
stage: JobRunStage;
|
|
113
|
+
/** The key the map or reduce stage was working on, when the failure belongs to one. */
|
|
114
|
+
key?: string;
|
|
115
|
+
message: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A run as anyone asking about it sees it: the record's own fields refined by what NetSuite says
|
|
119
|
+
* about the task. Every time is an ISO string, so this shape crosses to the browser unchanged.
|
|
120
|
+
*/
|
|
121
|
+
export interface JobRun<TResult = unknown, TExtra extends Record<string, unknown> = Record<string, never>> {
|
|
122
|
+
/** The run record's internal id: what startJob answers and a page polls with. */
|
|
123
|
+
id: string;
|
|
124
|
+
/** The job's name, as its declaration gives it. */
|
|
125
|
+
job: string;
|
|
126
|
+
status: JobRunStatus;
|
|
127
|
+
/** The stage the task is in, or null before it starts and after it ends. */
|
|
128
|
+
stage: JobRunStage | null;
|
|
129
|
+
/**
|
|
130
|
+
* How far the stage being processed has got, 0 to 100, as NetSuite reports it. It counts up inside a
|
|
131
|
+
* stage and starts again at the next one, so it is progress rather than a fraction of the whole run;
|
|
132
|
+
* a finished run reads 100. For something to put next to a progress bar, prefer the item counts.
|
|
133
|
+
*/
|
|
134
|
+
stagePercentComplete: number;
|
|
135
|
+
/** Rows the stage being processed has finished, or null when the task can no longer say. */
|
|
136
|
+
itemsProcessed: number | null;
|
|
137
|
+
/** Rows the stage being processed was given, or null when the task can no longer say: a run that has ended, or an id NetSuite has purged. */
|
|
138
|
+
itemsTotal: number | null;
|
|
139
|
+
/** The employee who started it, or null for a scheduled run. */
|
|
140
|
+
startedBy: number | null;
|
|
141
|
+
startedAt: string | null;
|
|
142
|
+
finishedAt: string | null;
|
|
143
|
+
/** The NetSuite task id, kept so a run can be asked about after the fact. */
|
|
144
|
+
taskId: string | null;
|
|
145
|
+
/** What summarize returned, once it has. */
|
|
146
|
+
result: TResult | null;
|
|
147
|
+
errors: JobRunError[];
|
|
148
|
+
/** The fields this application added to the run record, as netsuite-api.config.json declares them. */
|
|
149
|
+
extra: TExtra;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A run as a list shows it: the fields a query can read, so finding a run costs one query rather than a
|
|
153
|
+
* record load each. It carries no input, result or errors — a page finds a run here and then asks about
|
|
154
|
+
* it by id, which is also the only reading that consults the task, so a `running` row in a list is
|
|
155
|
+
* "last we knew", not a promise. Progress is not here either, for the same reason: only the task has it.
|
|
156
|
+
*/
|
|
157
|
+
export interface JobRunListEntry {
|
|
158
|
+
id: string;
|
|
159
|
+
job: string;
|
|
160
|
+
status: JobRunStatus;
|
|
161
|
+
stage: JobRunStage | null;
|
|
162
|
+
startedBy: number | null;
|
|
163
|
+
}
|
|
164
|
+
/** Which runs to list. Every field narrows; the newest are answered first. */
|
|
165
|
+
export interface JobRunQuery {
|
|
166
|
+
job?: string;
|
|
167
|
+
/** The employee who started them: how a page finds its own caller's runs again. */
|
|
168
|
+
startedBy?: number;
|
|
169
|
+
/** Only runs that have not ended, for "is one of these already going?". */
|
|
170
|
+
unfinishedOnly?: boolean;
|
|
171
|
+
/** How many, newest first. Defaults to 10, capped at 100. */
|
|
172
|
+
limit?: number;
|
|
173
|
+
}
|
|
174
|
+
/** One deployed job as server code starts it: `scripts.<job>` in the generated scripts map. */
|
|
175
|
+
export interface JobRef {
|
|
176
|
+
kind: 'mapreduce';
|
|
177
|
+
/** The job's name, as its declaration gives it and the run record records it. */
|
|
178
|
+
name: string;
|
|
179
|
+
scriptId: string;
|
|
180
|
+
/**
|
|
181
|
+
* Every deployment the job may run on, in the order they are tried. NetSuite runs one instance of
|
|
182
|
+
* a deployment at a time, so this list is how many runs of the job can overlap.
|
|
183
|
+
*/
|
|
184
|
+
deployments: readonly string[];
|
|
185
|
+
/** The script parameter the run id is passed in; every stage reads the run back from it. */
|
|
186
|
+
runParameter: string;
|
|
187
|
+
/** The job's own script parameters, by the name the stages use. */
|
|
188
|
+
parameters?: Readonly<Record<string, string>>;
|
|
189
|
+
}
|
|
190
|
+
/** One field an application added to its run record, as netsuite-api.config.json declares it. */
|
|
191
|
+
export interface JobRunExtraField {
|
|
192
|
+
id: string;
|
|
193
|
+
type: NetsuiteValueType;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The run record as this application deployed it, written by the generator from the `jobRuns` block
|
|
197
|
+
* of netsuite-api.config.json. The field names are the package's; their ids are the application's,
|
|
198
|
+
* because the record carries the application's prefix.
|
|
199
|
+
*/
|
|
200
|
+
export interface JobRunsConfig {
|
|
201
|
+
recordType: string;
|
|
202
|
+
fields: {
|
|
203
|
+
job: string;
|
|
204
|
+
status: string;
|
|
205
|
+
stage: string;
|
|
206
|
+
stagePercentComplete: string;
|
|
207
|
+
input: string;
|
|
208
|
+
result: string;
|
|
209
|
+
errors: string;
|
|
210
|
+
taskId: string;
|
|
211
|
+
deployment: string;
|
|
212
|
+
startedBy: string;
|
|
213
|
+
startedAt: string;
|
|
214
|
+
finishedAt: string;
|
|
215
|
+
};
|
|
216
|
+
/** Fields this application added, by the name the code uses for them. */
|
|
217
|
+
extraFields: Readonly<Record<string, JobRunExtraField>>;
|
|
218
|
+
}
|
|
@@ -6,4 +6,6 @@ export declare class ApiError extends Error {
|
|
|
6
6
|
static badRequest(message: string, details?: unknown): ApiError;
|
|
7
7
|
static notFound(message: string, details?: unknown): ApiError;
|
|
8
8
|
static forbidden(message?: string): ApiError;
|
|
9
|
+
/** The request was fine but the account cannot take it now: every deployment of a job is already running. */
|
|
10
|
+
static conflict(message: string, details?: unknown): ApiError;
|
|
9
11
|
}
|
package/dist/server/apiError.js
CHANGED
|
@@ -15,4 +15,8 @@ export class ApiError extends Error {
|
|
|
15
15
|
static forbidden(message = 'Not permitted') {
|
|
16
16
|
return new ApiError(403, message);
|
|
17
17
|
}
|
|
18
|
+
/** The request was fine but the account cannot take it now: every deployment of a job is already running. */
|
|
19
|
+
static conflict(message, details) {
|
|
20
|
+
return new ApiError(409, message, details);
|
|
21
|
+
}
|
|
18
22
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { EntryPoints } from 'N/types';
|
|
2
|
+
import type { JobParameterDeclaration, JobParameterValues, JobRunError, JobRunsConfig } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* A job is a Map/Reduce script written as stages instead of entry points:
|
|
5
|
+
*
|
|
6
|
+
* export const { getInputData, map, summarize } = defineJob({
|
|
7
|
+
* name: 'closeStaleOrders',
|
|
8
|
+
* scriptId: 'customscript_app_close_stale_orders_mr',
|
|
9
|
+
* deployments: ['customdeploy_app_close_stale_orders_mr', 'customdeploy_app_close_stale_orders_mr_2'],
|
|
10
|
+
* runParameter: 'custscript_app_close_stale_run',
|
|
11
|
+
* runs: jobRuns,
|
|
12
|
+
* }, {
|
|
13
|
+
* getInputData: (input: CloseStaleRequest): StaleOrder[] => listStaleOrders(input.olderThanDays),
|
|
14
|
+
* map: (order: StaleOrder, job) => { job.write(String(order.id), closeOrder(order.id)); },
|
|
15
|
+
* summarize: (summary): CloseStaleResult => ({ closed: summary.output.length }),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* What the wrapper adds is the run: the values between stages are typed and parsed, getInputData's
|
|
19
|
+
* input comes from the run record rather than a parameter the stage has to decode, summarize's
|
|
20
|
+
* return value becomes the run's result, and the errors NetSuite collected are written onto the run
|
|
21
|
+
* with it. A stage that throws marks the run failed and rethrows, so the failure is in the execution
|
|
22
|
+
* log where NetSuite puts it and on the run where the page is looking.
|
|
23
|
+
*
|
|
24
|
+
* Export the stages the job has. Every job exports summarize even when it has no summarize stage of
|
|
25
|
+
* its own, because that is where the run is closed. A stage exported but not declared throws when
|
|
26
|
+
* NetSuite calls it, rather than quietly passing values through.
|
|
27
|
+
*/
|
|
28
|
+
/** A job's script: what the generator reads, and what the run record's rows point back at. */
|
|
29
|
+
export interface JobDeclaration<TParameters extends Record<string, JobParameterDeclaration> = Record<string, never>> {
|
|
30
|
+
/** The job's name, as the run record records it and the scripts map keys it: the file name. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** The script record's id, `customscript_<prefix>_<name>_mr`. */
|
|
33
|
+
scriptId: string;
|
|
34
|
+
/**
|
|
35
|
+
* Every deployment of the script, in the order startJob tries them. NetSuite runs one instance of
|
|
36
|
+
* a deployment at a time, so the length of this list is how many runs can overlap; a job started
|
|
37
|
+
* from a page answers 409 when they are all busy.
|
|
38
|
+
*/
|
|
39
|
+
deployments: string[];
|
|
40
|
+
/** The script parameter carrying the run id, `custscript_<prefix>_<name>_run`. */
|
|
41
|
+
runParameter: string;
|
|
42
|
+
/** The job's own script parameters, by the name the stages use. */
|
|
43
|
+
parameters?: TParameters;
|
|
44
|
+
/** The application's run record, from the generated scripts map: `runs: jobRuns`. */
|
|
45
|
+
runs: JobRunsConfig;
|
|
46
|
+
}
|
|
47
|
+
/** What every stage is given besides its own values. */
|
|
48
|
+
export interface JobContext<TParameters extends Record<string, JobParameterDeclaration>> {
|
|
49
|
+
/** The run this stage belongs to, as the page polls it. */
|
|
50
|
+
readonly runId: string;
|
|
51
|
+
/** The script parameters the declaration names, typed. */
|
|
52
|
+
readonly parameters: JobParameterValues<TParameters>;
|
|
53
|
+
}
|
|
54
|
+
/** A stage that writes values for the stage after it. */
|
|
55
|
+
export interface JobWriteContext<TParameters extends Record<string, JobParameterDeclaration>, TValue> extends JobContext<TParameters> {
|
|
56
|
+
/** Hands one value to the next stage under a key; the value is carried as JSON and comes back typed. */
|
|
57
|
+
write(key: string, value: TValue): void;
|
|
58
|
+
}
|
|
59
|
+
/** What the run produced, as summarize sees it. */
|
|
60
|
+
export interface JobSummary<TOutput> {
|
|
61
|
+
/** Every key and value the reduce stage wrote, or the map stage's when the job has no reduce. */
|
|
62
|
+
output: {
|
|
63
|
+
key: string;
|
|
64
|
+
value: TOutput;
|
|
65
|
+
}[];
|
|
66
|
+
/** Everything that failed anywhere in the run; these are written onto the run whatever summarize returns. */
|
|
67
|
+
errors: JobRunError[];
|
|
68
|
+
seconds: number;
|
|
69
|
+
usage: number;
|
|
70
|
+
concurrency: number;
|
|
71
|
+
yields: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The stages of a job. getInputData's first parameter is the run's input and summarize's return type
|
|
75
|
+
* is the run's result: the generator reads both from those annotations, the way it reads an endpoint's
|
|
76
|
+
* request and response. The items getInputData returns are typed from there on.
|
|
77
|
+
*
|
|
78
|
+
* The values between map, reduce and summarize are carried as JSON, so each stage says what it
|
|
79
|
+
* expects: a reduce stage writes `values: number[]` and a summarize stage `summary: JobSummary<Total>`,
|
|
80
|
+
* and the wrapper hands them back as annotated. The stages are written as methods for that reason.
|
|
81
|
+
*/
|
|
82
|
+
export interface JobStages<TInput, TItem, TValue = unknown, TOutput = unknown, TResult = null, TParameters extends Record<string, JobParameterDeclaration> = Record<string, never>> {
|
|
83
|
+
getInputData(input: TInput, job: JobContext<TParameters>): TItem[] | Record<string, TItem>;
|
|
84
|
+
map?(item: TItem, job: JobWriteContext<TParameters, TValue>): void;
|
|
85
|
+
reduce?(key: string, values: TValue[], job: JobWriteContext<TParameters, TOutput>): void;
|
|
86
|
+
summarize?(summary: JobSummary<TOutput>, job: JobContext<TParameters>): TResult;
|
|
87
|
+
}
|
|
88
|
+
/** The four NetSuite entry points, of which a job file exports the ones it has. */
|
|
89
|
+
export interface JobEntryPoints {
|
|
90
|
+
getInputData: (context: EntryPoints.MapReduce.getInputDataContext) => unknown;
|
|
91
|
+
map: (context: EntryPoints.MapReduce.mapContext) => void;
|
|
92
|
+
reduce: (context: EntryPoints.MapReduce.reduceContext) => void;
|
|
93
|
+
summarize: (context: EntryPoints.MapReduce.summarizeContext) => void;
|
|
94
|
+
}
|
|
95
|
+
export declare function defineJob<TInput, TItem, TValue = unknown, TOutput = unknown, TResult = null, TParameters extends Record<string, JobParameterDeclaration> = Record<string, never>>(declaration: JobDeclaration<TParameters>, stages: JobStages<TInput, TItem, TValue, TOutput, TResult, TParameters>): JobEntryPoints;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as log from 'N/log';
|
|
2
|
+
import * as runtime from 'N/runtime';
|
|
3
|
+
import { createJobRunStore } from './jobRuns.js';
|
|
4
|
+
function readParameterValue(id, type) {
|
|
5
|
+
const raw = runtime.getCurrentScript().getParameter({ name: id });
|
|
6
|
+
if (raw === null || raw === undefined || raw === '')
|
|
7
|
+
return type === 'checkbox' ? false : type === 'integer' || type === 'decimal' ? 0 : '';
|
|
8
|
+
if (type === 'integer' || type === 'decimal')
|
|
9
|
+
return Number(raw);
|
|
10
|
+
if (type === 'checkbox')
|
|
11
|
+
return raw === true || raw === 'T' || raw === 'true';
|
|
12
|
+
if (raw instanceof Date)
|
|
13
|
+
return raw.toISOString();
|
|
14
|
+
return String(raw);
|
|
15
|
+
}
|
|
16
|
+
function describeError(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
/** The errors NetSuite collected, in the order the run met them. */
|
|
20
|
+
function readCollectedErrors(summary) {
|
|
21
|
+
var _a;
|
|
22
|
+
const errors = [];
|
|
23
|
+
if ((_a = summary.inputSummary) === null || _a === void 0 ? void 0 : _a.error)
|
|
24
|
+
errors.push({ stage: 'input', message: summary.inputSummary.error });
|
|
25
|
+
const collect = (stage, container) => {
|
|
26
|
+
container === null || container === void 0 ? void 0 : container.errors.iterator().each((key, error) => {
|
|
27
|
+
errors.push({ stage, key, message: error });
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
collect('map', summary.mapSummary);
|
|
32
|
+
collect('reduce', summary.reduceSummary);
|
|
33
|
+
return errors;
|
|
34
|
+
}
|
|
35
|
+
export function defineJob(declaration, stages) {
|
|
36
|
+
const store = createJobRunStore(declaration.runs);
|
|
37
|
+
const { name } = declaration;
|
|
38
|
+
const readParameters = () => {
|
|
39
|
+
var _a;
|
|
40
|
+
const values = {};
|
|
41
|
+
for (const [parameterName, parameter] of Object.entries((_a = declaration.parameters) !== null && _a !== void 0 ? _a : {}))
|
|
42
|
+
values[parameterName] = readParameterValue(parameter.id, parameter.type);
|
|
43
|
+
return values;
|
|
44
|
+
};
|
|
45
|
+
const readRunIdParameter = () => {
|
|
46
|
+
const raw = runtime.getCurrentScript().getParameter({ name: declaration.runParameter });
|
|
47
|
+
return typeof raw === 'string' && raw !== '' ? raw : undefined;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The run a stage belongs to. getInputData has it from the parameter or opens one; the stages
|
|
51
|
+
* after it read the parameter again, and a scheduled run (which has no parameter to read) is
|
|
52
|
+
* found by the deployment it is running on, one run at a time being all a deployment can do.
|
|
53
|
+
*/
|
|
54
|
+
const buildContext = (runIdSource) => {
|
|
55
|
+
let resolvedRunId;
|
|
56
|
+
let resolvedParameters;
|
|
57
|
+
return {
|
|
58
|
+
get runId() {
|
|
59
|
+
resolvedRunId = resolvedRunId !== null && resolvedRunId !== void 0 ? resolvedRunId : runIdSource();
|
|
60
|
+
return resolvedRunId;
|
|
61
|
+
},
|
|
62
|
+
get parameters() {
|
|
63
|
+
resolvedParameters = resolvedParameters !== null && resolvedParameters !== void 0 ? resolvedParameters : readParameters();
|
|
64
|
+
return resolvedParameters;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const lateRunId = () => {
|
|
69
|
+
var _a, _b;
|
|
70
|
+
const deployment = String(runtime.getCurrentScript().deploymentId);
|
|
71
|
+
return (_b = (_a = readRunIdParameter()) !== null && _a !== void 0 ? _a : store.findRunningRunId(name, deployment)) !== null && _b !== void 0 ? _b : '';
|
|
72
|
+
};
|
|
73
|
+
/** A stage's own failure: logged, written onto the run when the run is the only place it would show, and rethrown for NetSuite. */
|
|
74
|
+
const runStage = (stage, context, markRun, work) => {
|
|
75
|
+
try {
|
|
76
|
+
return work();
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = describeError(error);
|
|
80
|
+
log.error('job stage failed', { job: name, stage, runId: context.runId, message });
|
|
81
|
+
if (markRun && context.runId !== '')
|
|
82
|
+
store.fail(context.runId, { stage, message });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
getInputData(scriptContext) {
|
|
88
|
+
const deployment = String(runtime.getCurrentScript().deploymentId);
|
|
89
|
+
const runId = store.claimRun({ job: name, runId: readRunIdParameter(), deployment });
|
|
90
|
+
const context = buildContext(() => runId);
|
|
91
|
+
log.audit('job started', { job: name, runId, deployment, restarted: scriptContext.isRestarted });
|
|
92
|
+
return runStage('input', context, true, () => stages.getInputData(store.readInput(runId), context));
|
|
93
|
+
},
|
|
94
|
+
map(scriptContext) {
|
|
95
|
+
const context = buildContext(lateRunId);
|
|
96
|
+
runStage('map', context, false, () => {
|
|
97
|
+
if (!stages.map)
|
|
98
|
+
throw new Error(`${name} declares no map stage; remove map from this file's exports.`);
|
|
99
|
+
stages.map(JSON.parse(scriptContext.value), {
|
|
100
|
+
get runId() {
|
|
101
|
+
return context.runId;
|
|
102
|
+
},
|
|
103
|
+
get parameters() {
|
|
104
|
+
return context.parameters;
|
|
105
|
+
},
|
|
106
|
+
write: (key, value) => scriptContext.write({ key, value: JSON.stringify(value) }),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
reduce(scriptContext) {
|
|
111
|
+
const context = buildContext(lateRunId);
|
|
112
|
+
runStage('reduce', context, false, () => {
|
|
113
|
+
if (!stages.reduce)
|
|
114
|
+
throw new Error(`${name} declares no reduce stage; remove reduce from this file's exports.`);
|
|
115
|
+
stages.reduce(scriptContext.key, scriptContext.values.map((value) => JSON.parse(value)), {
|
|
116
|
+
get runId() {
|
|
117
|
+
return context.runId;
|
|
118
|
+
},
|
|
119
|
+
get parameters() {
|
|
120
|
+
return context.parameters;
|
|
121
|
+
},
|
|
122
|
+
write: (key, value) => scriptContext.write({ key, value: JSON.stringify(value) }),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
summarize(scriptContext) {
|
|
127
|
+
var _a;
|
|
128
|
+
const context = buildContext(lateRunId);
|
|
129
|
+
const errors = readCollectedErrors(scriptContext);
|
|
130
|
+
const output = [];
|
|
131
|
+
scriptContext.output.iterator().each((key, value) => {
|
|
132
|
+
output.push({ key, value: JSON.parse(value) });
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
const summary = {
|
|
136
|
+
output,
|
|
137
|
+
errors,
|
|
138
|
+
seconds: scriptContext.seconds,
|
|
139
|
+
usage: scriptContext.usage,
|
|
140
|
+
concurrency: scriptContext.concurrency,
|
|
141
|
+
yields: scriptContext.yields,
|
|
142
|
+
};
|
|
143
|
+
const result = runStage('summarize', context, true, () => (stages.summarize ? stages.summarize(summary, context) : null));
|
|
144
|
+
const status = ((_a = scriptContext.inputSummary) === null || _a === void 0 ? void 0 : _a.error) ? 'failed' : 'complete';
|
|
145
|
+
if (context.runId !== '')
|
|
146
|
+
store.finish(context.runId, { status, result, errors });
|
|
147
|
+
log.audit('job finished', { job: name, runId: context.runId, status, errors: errors.length, seconds: scriptContext.seconds, usage: scriptContext.usage });
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The server side of the API: declare a controller's endpoints, expose them as a Restlet or a
|
|
3
3
|
* Suitelet, reject a call with an ApiError, authorize calls, answer with a document instead of JSON,
|
|
4
|
-
* call another Suitelet controller from server code,
|
|
5
|
-
* inside NetSuite only; the modules here import
|
|
4
|
+
* call another Suitelet controller from server code, write a Map/Reduce job and the record its runs
|
|
5
|
+
* live in, and find a File Cabinet file by name. Runs inside NetSuite only; the modules here import
|
|
6
|
+
* N/*.
|
|
6
7
|
*/
|
|
7
8
|
export { ApiError } from './apiError.js';
|
|
8
9
|
export { defineEndpoints, invokeEndpoint, parseEndpointRequest, readEndpointCall } from './endpoint.js';
|
|
9
10
|
export type { AuthorizeEndpoint, ControllerOptions, EndpointCall, EndpointCallContext, EndpointOutcome, InvokeEndpointOptions } from './endpoint.js';
|
|
11
|
+
export { defineJob } from './defineJob.js';
|
|
12
|
+
export type { JobContext, JobDeclaration, JobEntryPoints, JobStages, JobSummary, JobWriteContext } from './defineJob.js';
|
|
13
|
+
export { createJobRunStore } from './jobRuns.js';
|
|
14
|
+
export type { ClaimRunDetails, FinishRunOutcome, JobRunStore, StartJobOptions } from './jobRuns.js';
|
|
10
15
|
export { defineRestlet } from './defineRestlet.js';
|
|
11
16
|
export type { RestletEntryPoint } from './defineRestlet.js';
|
|
12
17
|
export { defineSuitelet } from './defineSuitelet.js';
|
|
@@ -18,4 +23,4 @@ export type { SuiteletClient } from './suiteletClient.js';
|
|
|
18
23
|
export { findFileId, findFolderId, getFileUrlByName } from './fileCabinet.js';
|
|
19
24
|
export type { FileCabinetLocation } from './fileCabinet.js';
|
|
20
25
|
export { ENDPOINT_PARAMETER } from '../index.js';
|
|
21
|
-
export type { ApiEnvelope, ApiErrorBody, Endpoint, Endpoints, EndpointRequest, EndpointResponse, RawResponse, ScriptDeclaration, ScriptKind, ScriptRef } from '../index.js';
|
|
26
|
+
export type { ApiEnvelope, ApiErrorBody, Endpoint, Endpoints, EndpointRequest, EndpointResponse, JobParameterDeclaration, JobParameterValue, JobParameterValues, JobRef, JobRun, JobRunError, JobRunListEntry, JobRunQuery, JobRunExtraField, JobRunsConfig, JobRunStage, JobRunStatus, NetsuiteValueType, RawResponse, ScriptDeclaration, ScriptKind, ScriptRef, } from '../index.js';
|
package/dist/server/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The server side of the API: declare a controller's endpoints, expose them as a Restlet or a
|
|
3
3
|
* Suitelet, reject a call with an ApiError, authorize calls, answer with a document instead of JSON,
|
|
4
|
-
* call another Suitelet controller from server code,
|
|
5
|
-
* inside NetSuite only; the modules here import
|
|
4
|
+
* call another Suitelet controller from server code, write a Map/Reduce job and the record its runs
|
|
5
|
+
* live in, and find a File Cabinet file by name. Runs inside NetSuite only; the modules here import
|
|
6
|
+
* N/*.
|
|
6
7
|
*/
|
|
7
8
|
export { ApiError } from './apiError.js';
|
|
8
9
|
export { defineEndpoints, invokeEndpoint, parseEndpointRequest, readEndpointCall } from './endpoint.js';
|
|
10
|
+
export { defineJob } from './defineJob.js';
|
|
11
|
+
export { createJobRunStore } from './jobRuns.js';
|
|
9
12
|
export { defineRestlet } from './defineRestlet.js';
|
|
10
13
|
export { defineSuitelet } from './defineSuitelet.js';
|
|
11
14
|
export { isRawResponse, rawResponse, writeRawResponse } from './rawResponse.js';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { JobRef, JobRun, JobRunError, JobRunListEntry, JobRunQuery, JobRunStatus, JobRunsConfig } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The run record: everything a job run is, outside the job. A Map/Reduce script answers nothing and
|
|
4
|
+
* cannot be waited on, so starting one writes a record first and passes its id in as the script
|
|
5
|
+
* parameter; the stages read their input back from it and summarize writes the result to it. Reading
|
|
6
|
+
* a run asks NetSuite about the task as well, which is what tells a run that died apart from one
|
|
7
|
+
* still working: a record left at `running` under a task NetSuite has finished or given up on is a
|
|
8
|
+
* failure, not something to keep polling.
|
|
9
|
+
*
|
|
10
|
+
* The record is the application's own (`customrecord_<prefix>_job_run`), so the ids come in as the
|
|
11
|
+
* generated `jobRuns` config rather than being written here. A repository builds the store once and
|
|
12
|
+
* the jobs build their own from the same config; nothing is global.
|
|
13
|
+
*/
|
|
14
|
+
/** What startJob may add to the run beyond the input: the application's own fields on the run record. */
|
|
15
|
+
export interface StartJobOptions {
|
|
16
|
+
/** Values for the fields declared in `jobRuns.extraFields`, by name. */
|
|
17
|
+
extra?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/** What a stage's wrapper needs to find or open a run; not used by application code. */
|
|
20
|
+
export interface ClaimRunDetails {
|
|
21
|
+
job: string;
|
|
22
|
+
/** The run id the script parameter carried, or undefined for a scheduled run, which opens its own. */
|
|
23
|
+
runId?: string;
|
|
24
|
+
deployment: string;
|
|
25
|
+
}
|
|
26
|
+
/** How a run ended, as summarize leaves it. */
|
|
27
|
+
export interface FinishRunOutcome {
|
|
28
|
+
status: Extract<JobRunStatus, 'complete' | 'failed'>;
|
|
29
|
+
result: unknown;
|
|
30
|
+
errors: JobRunError[];
|
|
31
|
+
}
|
|
32
|
+
export interface JobRunStore {
|
|
33
|
+
/**
|
|
34
|
+
* Starts a job: writes the run, then submits the task to the first deployment that takes it. The
|
|
35
|
+
* run id is what a page polls with. Throws a 409 when every deployment is already running, and
|
|
36
|
+
* leaves no run behind when it does, because nothing started.
|
|
37
|
+
*/
|
|
38
|
+
start(job: JobRef, input?: unknown, options?: StartJobOptions): string;
|
|
39
|
+
/** The run as anyone asking sees it: the record refined by what NetSuite says about the task. Null when the record is gone (cleaned up, or never existed). */
|
|
40
|
+
read<TResult = unknown, TExtra extends Record<string, unknown> = Record<string, never>>(runId: string): JobRun<TResult, TExtra> | null;
|
|
41
|
+
/**
|
|
42
|
+
* The runs matching the query, newest first: one query, no record loads, so a page can find the run it
|
|
43
|
+
* lost track of. The rows carry what the record says and not what the task says, so read a run by id
|
|
44
|
+
* before believing it is still working.
|
|
45
|
+
*/
|
|
46
|
+
findRuns(runQuery?: JobRunQuery): JobRunListEntry[];
|
|
47
|
+
/** Run ids older than the given number of days, for the cleanup job. */
|
|
48
|
+
findExpired(olderThanDays: number): string[];
|
|
49
|
+
remove(runId: string): void;
|
|
50
|
+
/** Opens the run a stage belongs to, creating one for a scheduled run. Called by the job wrapper. */
|
|
51
|
+
claimRun(details: ClaimRunDetails): string;
|
|
52
|
+
/** The input the run was started with; `{}` for a scheduled run. Called by the job wrapper. */
|
|
53
|
+
readInput<TInput>(runId: string): TInput;
|
|
54
|
+
/** The run id of whatever is running on this deployment, for the stages after getInputData. Called by the job wrapper. */
|
|
55
|
+
findRunningRunId(job: string, deployment: string): string | undefined;
|
|
56
|
+
/** Writes the result and the errors, and closes the run. Called by the job wrapper. */
|
|
57
|
+
finish(runId: string, outcome: FinishRunOutcome): void;
|
|
58
|
+
/** Marks the run failed with one error, for a stage that threw. Called by the job wrapper. */
|
|
59
|
+
fail(runId: string, error: JobRunError): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The store for one application's run record. Build it once where it is used: a repository for the
|
|
63
|
+
* application's own calls, and the job wrapper for the stages.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createJobRunStore(config: JobRunsConfig): JobRunStore;
|