@amerilux/netsuite-api 0.4.0 → 0.6.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 +101 -6
- 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 +91 -3
- 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/file-system.d.ts +2 -0
- package/dist-tooling/file-system.js +23 -0
- package/dist-tooling/generate.d.ts +10 -1
- package/dist-tooling/generate.js +145 -25
- package/dist-tooling/index.d.ts +7 -4
- package/dist-tooling/index.js +3 -1
- package/dist-tooling/jobReader.d.ts +83 -0
- package/dist-tooling/jobReader.js +386 -0
- package/package.json +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import * as nodePath from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { hasExportModifier, readLeadingJsDoc, readScriptTypeHeader, readStringProperty, readTypeImport } from './controllerReader.js';
|
|
4
|
+
import { toPosixPath } from './file-system.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reads what a job run carries: the script it declares in its defineJob call, the stages it has, and
|
|
7
|
+
* the types on either end of a run. A run's input is the first parameter of getInputData and its result
|
|
8
|
+
* is what summarize returns, so those two annotations are the contract, the way a handler's parameter
|
|
9
|
+
* and return type are a controller's. A parse, not a type check: the ids are string literals and the
|
|
10
|
+
* shapes are written out.
|
|
11
|
+
*
|
|
12
|
+
* A job is a folder — `<name>/<name>.ts` declares it — and a stage is either a function written inline
|
|
13
|
+
* or one imported from a file beside it (`./map`), which is where the annotations are then read from. So
|
|
14
|
+
* a developer opens map.ts to see what the map stage does, and the definition file stays a declaration.
|
|
15
|
+
*/
|
|
16
|
+
/** The stage names a job may declare, in the order NetSuite calls them. */
|
|
17
|
+
export const JOB_STAGE_NAMES = ['getInputData', 'map', 'reduce', 'summarize'];
|
|
18
|
+
/** The script type a job's leading JSDoc must declare. */
|
|
19
|
+
export const JOB_SCRIPT_TYPE_HEADER = 'MapReduceScript';
|
|
20
|
+
/** The type the generated job module gives the run's input, and the one it gives the result. */
|
|
21
|
+
export const GENERATED_JOB_RESULT_TYPE_NAME = 'Result';
|
|
22
|
+
const jobFileNamePattern = /^([a-z][A-Za-z0-9]*)\.ts$/;
|
|
23
|
+
const jobFolderNamePattern = /^[a-z][A-Za-z0-9]*$/;
|
|
24
|
+
const netsuiteValueTypes = ['text', 'integer', 'decimal', 'checkbox', 'date', 'select'];
|
|
25
|
+
/** The name a job folder declares: `approveOrders` for api/src/jobs/approveOrders, or undefined when it is not a name. */
|
|
26
|
+
export function readJobFolderName(folderName) {
|
|
27
|
+
return jobFolderNamePattern.test(folderName) ? folderName : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** The file that declares the job of a folder: the folder's own name again, so the file is unique to open and to search for. */
|
|
30
|
+
export function jobDefinitionFileName(jobName) {
|
|
31
|
+
return `${jobName}.ts`;
|
|
32
|
+
}
|
|
33
|
+
/** `export const { getInputData, map, summarize } = defineJob(...)`: the call, and the names it exports. */
|
|
34
|
+
function findDefineJobCall(statement) {
|
|
35
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
36
|
+
const initializer = declaration.initializer;
|
|
37
|
+
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || initializer.expression.text !== 'defineJob')
|
|
38
|
+
continue;
|
|
39
|
+
if (!ts.isObjectBindingPattern(declaration.name))
|
|
40
|
+
return { call: initializer, exportedStages: [], destructured: false };
|
|
41
|
+
const exportedStages = declaration.name.elements
|
|
42
|
+
.map((element) => (ts.isIdentifier(element.name) ? element.name.text : undefined))
|
|
43
|
+
.filter((exported) => exported !== undefined);
|
|
44
|
+
return { call: initializer, exportedStages, destructured: true };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function readStringArrayProperty(literal, propertyName) {
|
|
49
|
+
for (const property of literal.properties) {
|
|
50
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || property.name.text !== propertyName)
|
|
51
|
+
continue;
|
|
52
|
+
if (!ts.isArrayLiteralExpression(property.initializer))
|
|
53
|
+
return undefined;
|
|
54
|
+
const values = property.initializer.elements.map((element) => (ts.isStringLiteral(element) ? element.text : undefined));
|
|
55
|
+
return values.every((value) => value !== undefined) ? values : undefined;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function hasProperty(literal, propertyName) {
|
|
60
|
+
return literal.properties.some((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === propertyName);
|
|
61
|
+
}
|
|
62
|
+
function readObjectProperty(literal, propertyName) {
|
|
63
|
+
for (const property of literal.properties) {
|
|
64
|
+
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === propertyName && ts.isObjectLiteralExpression(property.initializer)) {
|
|
65
|
+
return property.initializer;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function readParameters(declaration, filePath) {
|
|
71
|
+
const problems = [];
|
|
72
|
+
const parameters = [];
|
|
73
|
+
const literal = readObjectProperty(declaration, 'parameters');
|
|
74
|
+
if (!literal) {
|
|
75
|
+
if (hasProperty(declaration, 'parameters'))
|
|
76
|
+
problems.push({ filePath, message: "'parameters' must be an object literal of { id, type } written inline; the generator reads the ids from it." });
|
|
77
|
+
return { parameters, problems };
|
|
78
|
+
}
|
|
79
|
+
for (const property of literal.properties) {
|
|
80
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
81
|
+
problems.push({ filePath, message: 'every script parameter is named by a plain identifier and declared as { id, type }.' });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const name = property.name.text;
|
|
85
|
+
if (!ts.isObjectLiteralExpression(property.initializer)) {
|
|
86
|
+
problems.push({ filePath, message: `parameter '${name}' must be written as { id: 'custscript_...', type: '...' }.` });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const id = readStringProperty(property.initializer, 'id');
|
|
90
|
+
const type = readStringProperty(property.initializer, 'type');
|
|
91
|
+
if (id === undefined)
|
|
92
|
+
problems.push({ filePath, message: `parameter '${name}' needs 'id' as a string literal: the script parameter's id in NetSuite.` });
|
|
93
|
+
if (type === undefined || !netsuiteValueTypes.includes(type)) {
|
|
94
|
+
problems.push({ filePath, message: `parameter '${name}' needs 'type' as one of ${netsuiteValueTypes.map((value) => `'${value}'`).join(', ')}.` });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (id !== undefined)
|
|
98
|
+
parameters.push({ name, id, type: type });
|
|
99
|
+
}
|
|
100
|
+
return { parameters, problems };
|
|
101
|
+
}
|
|
102
|
+
function readJobDeclaration(declaration, jobName, filePath) {
|
|
103
|
+
const problems = [];
|
|
104
|
+
const name = readStringProperty(declaration, 'name');
|
|
105
|
+
const scriptId = readStringProperty(declaration, 'scriptId');
|
|
106
|
+
const runParameter = readStringProperty(declaration, 'runParameter');
|
|
107
|
+
const deployments = readStringArrayProperty(declaration, 'deployments');
|
|
108
|
+
if (name === undefined)
|
|
109
|
+
problems.push({ filePath, message: "the job declaration needs 'name' as a string literal; the generator reads it from the source." });
|
|
110
|
+
else if (name !== jobName)
|
|
111
|
+
problems.push({ filePath, message: `the job declaration says name: '${name}' but the job is ${jobName}; the name is the job's folder and file name.` });
|
|
112
|
+
if (scriptId === undefined)
|
|
113
|
+
problems.push({ filePath, message: "the job declaration needs 'scriptId' as a string literal." });
|
|
114
|
+
if (runParameter === undefined) {
|
|
115
|
+
problems.push({ filePath, message: "the job declaration needs 'runParameter' as a string literal: the script parameter the run id is passed in." });
|
|
116
|
+
}
|
|
117
|
+
if (deployments === undefined) {
|
|
118
|
+
problems.push({ filePath, message: "the job declaration needs 'deployments' as an array of string literals: every deployment a run may be started on." });
|
|
119
|
+
}
|
|
120
|
+
else if (deployments.length === 0) {
|
|
121
|
+
problems.push({ filePath, message: "'deployments' is empty; a job needs at least one deployment to run on." });
|
|
122
|
+
}
|
|
123
|
+
if (!hasProperty(declaration, 'runs')) {
|
|
124
|
+
problems.push({ filePath, message: "the job declaration needs 'runs: jobRuns', the run record from the generated scripts map; the stages read the run through it." });
|
|
125
|
+
}
|
|
126
|
+
const readParametersResult = readParameters(declaration, filePath);
|
|
127
|
+
problems.push(...readParametersResult.problems);
|
|
128
|
+
if (problems.length > 0 || scriptId === undefined || runParameter === undefined || deployments === undefined)
|
|
129
|
+
return { problems };
|
|
130
|
+
return { script: { scriptId, deployments, runParameter, parameters: readParametersResult.parameters }, problems };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The exported type declarations of a file and the type imports it carries, for a job's definition file
|
|
134
|
+
* or for one of its stage files: both end up in the browser's module, so both are read the same way.
|
|
135
|
+
*/
|
|
136
|
+
function readFileTypes(sourceFile, filePath, options) {
|
|
137
|
+
const declarations = [];
|
|
138
|
+
const carriedTypeImports = [];
|
|
139
|
+
const inlinedTypeImports = [];
|
|
140
|
+
const problems = [];
|
|
141
|
+
for (const statement of sourceFile.statements) {
|
|
142
|
+
if (ts.isImportDeclaration(statement)) {
|
|
143
|
+
const result = readTypeImport(statement, filePath, options);
|
|
144
|
+
problems.push(...result.problems);
|
|
145
|
+
if (result.read?.kind === 'carried')
|
|
146
|
+
carriedTypeImports.push(result.read.typeImport);
|
|
147
|
+
else if (result.read?.kind === 'inlined')
|
|
148
|
+
inlinedTypeImports.push(result.read.typeImport);
|
|
149
|
+
else if (result.read?.kind === 'controller') {
|
|
150
|
+
problems.push({ filePath, message: "a job does not take types from a controller; its input and result are its own, or a service's." });
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement) && !ts.isEnumDeclaration(statement))
|
|
155
|
+
continue;
|
|
156
|
+
if (!hasExportModifier(statement)) {
|
|
157
|
+
problems.push({ filePath, message: `'${statement.name.text}' is not exported; the generator copies the shapes a run's result names into the browser's module as they are written, so export them.` });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (statement.name.text === GENERATED_JOB_RESULT_TYPE_NAME) {
|
|
161
|
+
problems.push({ filePath, message: `type '${statement.name.text}' is the name the generated module gives the run's result; call this shape something else.` });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const jsDoc = readLeadingJsDoc(statement, sourceFile);
|
|
165
|
+
declarations.push({ name: statement.name.text, text: `${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}` });
|
|
166
|
+
}
|
|
167
|
+
return { declarations, carriedTypeImports, inlinedTypeImports, problems };
|
|
168
|
+
}
|
|
169
|
+
/** `import { mapFunction } from './map'`: every value imported by name, by the name this file calls it. */
|
|
170
|
+
function readValueImports(sourceFile) {
|
|
171
|
+
const valueImports = new Map();
|
|
172
|
+
for (const statement of sourceFile.statements) {
|
|
173
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
174
|
+
continue;
|
|
175
|
+
const clause = statement.importClause;
|
|
176
|
+
if (!clause || clause.isTypeOnly || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
177
|
+
continue;
|
|
178
|
+
for (const element of clause.namedBindings.elements) {
|
|
179
|
+
if (element.isTypeOnly)
|
|
180
|
+
continue;
|
|
181
|
+
valueImports.set(element.name.text, { specifier: statement.moduleSpecifier.text, importedName: (element.propertyName ?? element.name).text });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return valueImports;
|
|
185
|
+
}
|
|
186
|
+
/** The exported function of that name in a stage file: `export const mapFunction = …` or `export function mapFunction…`. */
|
|
187
|
+
function findExportedFunction(sourceFile, name) {
|
|
188
|
+
for (const statement of sourceFile.statements) {
|
|
189
|
+
if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement) && statement.name?.text === name)
|
|
190
|
+
return statement;
|
|
191
|
+
if (!ts.isVariableStatement(statement) || !hasExportModifier(statement))
|
|
192
|
+
continue;
|
|
193
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
194
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name)
|
|
195
|
+
continue;
|
|
196
|
+
const initializer = declaration.initializer;
|
|
197
|
+
if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)))
|
|
198
|
+
return initializer;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
function readStages(literal, filePath, sourceFile, options) {
|
|
204
|
+
const problems = [];
|
|
205
|
+
const stages = [];
|
|
206
|
+
const stageFiles = [];
|
|
207
|
+
const valueImports = readValueImports(sourceFile);
|
|
208
|
+
const filesBySpecifier = new Map();
|
|
209
|
+
let inputType;
|
|
210
|
+
let resultType;
|
|
211
|
+
/** A stage file, read once however many stages come from it, and its shapes collected as it is read. */
|
|
212
|
+
function readStageFile(specifier) {
|
|
213
|
+
const alreadyRead = filesBySpecifier.get(specifier);
|
|
214
|
+
if (alreadyRead)
|
|
215
|
+
return alreadyRead;
|
|
216
|
+
const read = options.readStageFile?.(specifier);
|
|
217
|
+
if (!read)
|
|
218
|
+
return undefined;
|
|
219
|
+
const stageSourceFile = ts.createSourceFile(read.filePath, read.source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
220
|
+
const types = readFileTypes(stageSourceFile, read.filePath, options);
|
|
221
|
+
problems.push(...types.problems);
|
|
222
|
+
stageFiles.push({ filePath: read.filePath, declarations: types.declarations, inlinedTypeImports: types.inlinedTypeImports, carriedTypeImports: types.carriedTypeImports });
|
|
223
|
+
const entry = { sourceFile: stageSourceFile, filePath: read.filePath };
|
|
224
|
+
filesBySpecifier.set(specifier, entry);
|
|
225
|
+
return entry;
|
|
226
|
+
}
|
|
227
|
+
/** The function a stage names, in the file it is imported from: `getInputData: getInputDataFunction`. */
|
|
228
|
+
function findReferencedStage(stageName, referenced) {
|
|
229
|
+
const imported = valueImports.get(referenced);
|
|
230
|
+
if (!imported) {
|
|
231
|
+
problems.push({ filePath, message: `stage '${stageName}' names '${referenced}', which this file does not import; a stage is written inline or imported from a file beside the job.` });
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
if (!imported.specifier.startsWith('./') && !imported.specifier.startsWith('../')) {
|
|
235
|
+
problems.push({ filePath, message: `stage '${stageName}' comes from '${imported.specifier}'; a stage is imported from a file beside the job, such as './${stageName}'.` });
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const stageFile = readStageFile(imported.specifier);
|
|
239
|
+
if (!stageFile) {
|
|
240
|
+
problems.push({ filePath, message: `stage '${stageName}' is imported from '${imported.specifier}', which is not a file beside the job the generator can read.` });
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const handler = findExportedFunction(stageFile.sourceFile, imported.importedName);
|
|
244
|
+
if (!handler) {
|
|
245
|
+
problems.push({ filePath: stageFile.filePath, message: `'${imported.importedName}' is not exported from here as a function, and stage '${stageName}' names it; its annotations are the run's contract.` });
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return { handler, sourceFile: stageFile.sourceFile, filePath: stageFile.filePath };
|
|
249
|
+
}
|
|
250
|
+
for (const property of literal.properties) {
|
|
251
|
+
const stageName = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined;
|
|
252
|
+
if (stageName === undefined || !JOB_STAGE_NAMES.includes(stageName)) {
|
|
253
|
+
problems.push({ filePath, message: `'${stageName ?? 'a stage'}' is not a stage; a job declares ${JOB_STAGE_NAMES.join(', ')}.` });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// Where the annotations are read from: the definition file for an inline stage, the stage's own file otherwise.
|
|
257
|
+
let handler;
|
|
258
|
+
let annotatedIn = { sourceFile, filePath };
|
|
259
|
+
if (ts.isMethodDeclaration(property))
|
|
260
|
+
handler = property;
|
|
261
|
+
else if (ts.isPropertyAssignment(property) && (ts.isArrowFunction(property.initializer) || ts.isFunctionExpression(property.initializer)))
|
|
262
|
+
handler = property.initializer;
|
|
263
|
+
else {
|
|
264
|
+
const referenced = ts.isShorthandPropertyAssignment(property)
|
|
265
|
+
? property.name.text
|
|
266
|
+
: ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)
|
|
267
|
+
? property.initializer.text
|
|
268
|
+
: undefined;
|
|
269
|
+
if (referenced === undefined) {
|
|
270
|
+
problems.push({ filePath, message: `stage '${stageName}' is neither a function nor the name of one; write it inline or import it from a file beside the job.` });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const found = findReferencedStage(stageName, referenced);
|
|
274
|
+
if (!found)
|
|
275
|
+
continue;
|
|
276
|
+
handler = found.handler;
|
|
277
|
+
annotatedIn = { sourceFile: found.sourceFile, filePath: found.filePath };
|
|
278
|
+
}
|
|
279
|
+
stages.push(stageName);
|
|
280
|
+
if (stageName === 'getInputData') {
|
|
281
|
+
const parameter = handler.parameters[0];
|
|
282
|
+
if (parameter && !parameter.type) {
|
|
283
|
+
problems.push({ filePath: annotatedIn.filePath, message: "getInputData has no type on its first parameter; a run's input shape is read from it." });
|
|
284
|
+
}
|
|
285
|
+
inputType = parameter?.type?.getText(annotatedIn.sourceFile);
|
|
286
|
+
}
|
|
287
|
+
if (stageName === 'summarize') {
|
|
288
|
+
if (!handler.type)
|
|
289
|
+
problems.push({ filePath: annotatedIn.filePath, message: "summarize has no return type annotation; a run's result shape is read from it." });
|
|
290
|
+
resultType = handler.type?.getText(annotatedIn.sourceFile);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!stages.includes('getInputData'))
|
|
294
|
+
problems.push({ filePath, message: 'a job declares getInputData; it is what NetSuite asks for the run\'s work.' });
|
|
295
|
+
if (!stages.includes('map') && !stages.includes('reduce')) {
|
|
296
|
+
problems.push({ filePath, message: 'a job declares a map stage, a reduce stage, or both; NetSuite has nothing to run otherwise.' });
|
|
297
|
+
}
|
|
298
|
+
return { stages, inputType, resultType, stageFiles, problems };
|
|
299
|
+
}
|
|
300
|
+
export function readJobContract(filePath, source, options) {
|
|
301
|
+
const problems = [];
|
|
302
|
+
const posixPath = toPosixPath(filePath);
|
|
303
|
+
const fileName = nodePath.basename(posixPath);
|
|
304
|
+
const nameMatch = jobFileNamePattern.exec(fileName);
|
|
305
|
+
if (!nameMatch)
|
|
306
|
+
return { problems: [{ filePath, message: 'a job is declared in <name>.ts, with <name> in camelCase; nothing else declares a job.' }] };
|
|
307
|
+
const name = nameMatch[1];
|
|
308
|
+
const folderName = nodePath.basename(nodePath.dirname(posixPath));
|
|
309
|
+
if (folderName !== name) {
|
|
310
|
+
problems.push({ filePath, message: `a job lives in a folder of its own name: ${fileName} belongs in a folder called ${name}, beside the stage files it is made of.` });
|
|
311
|
+
}
|
|
312
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
313
|
+
const header = readScriptTypeHeader(source);
|
|
314
|
+
if (header !== JOB_SCRIPT_TYPE_HEADER) {
|
|
315
|
+
problems.push({ filePath, message: `the leading JSDoc says '@NScriptType ${header ?? '(none)'}'; a job is a ${JOB_SCRIPT_TYPE_HEADER}.` });
|
|
316
|
+
}
|
|
317
|
+
const fileTypes = readFileTypes(sourceFile, filePath, options);
|
|
318
|
+
problems.push(...fileTypes.problems);
|
|
319
|
+
const { carriedTypeImports, inlinedTypeImports, declarations: typeDeclarations } = fileTypes;
|
|
320
|
+
let contract;
|
|
321
|
+
for (const statement of sourceFile.statements) {
|
|
322
|
+
if (!ts.isVariableStatement(statement))
|
|
323
|
+
continue;
|
|
324
|
+
const found = findDefineJobCall(statement);
|
|
325
|
+
if (!found)
|
|
326
|
+
continue;
|
|
327
|
+
if (contract) {
|
|
328
|
+
problems.push({ filePath, message: 'a job file has one defineJob call; a second one is a second script.' });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (!hasExportModifier(statement) || !found.destructured) {
|
|
332
|
+
problems.push({ filePath, message: "the stages are exported from the call: `export const { getInputData, map, summarize } = defineJob({ ... }, { ... })`; NetSuite looks for those exports." });
|
|
333
|
+
}
|
|
334
|
+
const [declarationArgument, stagesArgument] = found.call.arguments;
|
|
335
|
+
if (!declarationArgument || !ts.isObjectLiteralExpression(declarationArgument)) {
|
|
336
|
+
problems.push({ filePath, message: 'defineJob takes the job declaration as an object literal written inline.' });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!stagesArgument || !ts.isObjectLiteralExpression(stagesArgument)) {
|
|
340
|
+
problems.push({ filePath, message: 'defineJob takes the stages as an object literal written inline, each stage a function with its types annotated.' });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const declared = readJobDeclaration(declarationArgument, name, filePath);
|
|
344
|
+
problems.push(...declared.problems);
|
|
345
|
+
const stages = readStages(stagesArgument, filePath, sourceFile, options);
|
|
346
|
+
problems.push(...stages.problems);
|
|
347
|
+
for (const exported of found.exportedStages) {
|
|
348
|
+
if (!JOB_STAGE_NAMES.includes(exported)) {
|
|
349
|
+
problems.push({ filePath, message: `'${exported}' is not a stage, so NetSuite has no entry point by that name; export ${JOB_STAGE_NAMES.join(', ')}.` });
|
|
350
|
+
}
|
|
351
|
+
else if (exported !== 'summarize' && !stages.stages.includes(exported)) {
|
|
352
|
+
problems.push({ filePath, message: `'${exported}' is exported but not declared; a stage exported without a declaration throws when NetSuite calls it.` });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const stage of stages.stages) {
|
|
356
|
+
if (!found.exportedStages.includes(stage)) {
|
|
357
|
+
problems.push({ filePath, message: `stage '${stage}' is declared but not exported; NetSuite only runs the stages the file exports.` });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// The run is closed in summarize, so a job that leaves it out would leave every run of it looking unfinished.
|
|
361
|
+
if (!found.exportedStages.includes('summarize')) {
|
|
362
|
+
problems.push({ filePath, message: 'every job exports summarize, declared or not: it is where the run is closed and its result written.' });
|
|
363
|
+
}
|
|
364
|
+
if (!declared.script)
|
|
365
|
+
continue;
|
|
366
|
+
contract = {
|
|
367
|
+
name,
|
|
368
|
+
filePath,
|
|
369
|
+
script: declared.script,
|
|
370
|
+
inputType: stages.inputType,
|
|
371
|
+
resultType: stages.resultType,
|
|
372
|
+
stages: stages.stages,
|
|
373
|
+
exportedStages: found.exportedStages,
|
|
374
|
+
stageFiles: stages.stageFiles,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (!contract) {
|
|
378
|
+
if (problems.length === 0) {
|
|
379
|
+
problems.push({ filePath, message: 'must declare its script: `export const { getInputData, map, summarize } = defineJob({ name, scriptId, deployments, runParameter, runs }, { ... })`.' });
|
|
380
|
+
}
|
|
381
|
+
return { problems };
|
|
382
|
+
}
|
|
383
|
+
if (problems.length > 0)
|
|
384
|
+
return { problems };
|
|
385
|
+
return { contract: { ...contract, carriedTypeImports, inlinedTypeImports, typeDeclarations }, problems };
|
|
386
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amerilux/netsuite-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The API layer for a NetSuite single-page app: endpoints served by a Restlet or a Suitelet, a typed browser client, SuiteScript module stubs for tests, and a generator that writes the client module from the controllers.",
|
|
6
6
|
"license": "MIT",
|