@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/dist-tooling/index.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/** The generator as a library: what the `netsuite-api` command runs, for a build tool or a test to call directly. */
|
|
2
2
|
export { DEFAULT_CONFIG_FILE_NAME, ClientGeneratorConfigError, defaultClientGeneratorConfig, loadClientGeneratorConfig } from './config.js';
|
|
3
|
-
export
|
|
3
|
+
export { JOB_RUN_FIELD_SUFFIXES, netsuiteValueTypeNames, resolveJobRunFieldIds } from './config.js';
|
|
4
|
+
export type { ClientGeneratorConfig, JobRunFieldName, JobRunsSettings, NetsuiteValueTypeName, ResolvedClientGeneratorConfig } from './config.js';
|
|
4
5
|
export { GENERATED_CLIENT_NAME, GENERATED_ENDPOINTS_TYPE_NAME, RAW_RESPONSE_TYPE_NAME, isControllerFileName, readControllerContract, readLeadingJsDoc } from './controllerReader.js';
|
|
5
6
|
export type { CarriedTypeImport, ControllerContract, ControllerKind, ControllerProblem, ControllerReadResult, ControllerTypeImport, DeclaredScript, EndpointSignature, InlinedTypeImport, ReadControllerOptions, TypeDeclaration, TypeImportName } from './controllerReader.js';
|
|
7
|
+
export { GENERATED_JOB_RESULT_TYPE_NAME, JOB_SCRIPT_TYPE_HEADER, JOB_STAGE_NAMES, isJobFileName, readJobContract } from './jobReader.js';
|
|
8
|
+
export type { DeclaredJob, JobContract, JobParameterContract, JobReadResult, JobStageName } from './jobReader.js';
|
|
6
9
|
export { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
|
|
7
10
|
export type { InlinableTypeDeclaration, InlinableTypesFile, SelectedInlinedTypes } from './typesFileReader.js';
|
|
8
|
-
export { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitScriptsModule } from './emit.js';
|
|
9
|
-
export type { EmitClientIndexModuleOptions, EmitControllerModuleOptions, EmitScriptsModuleOptions, EmittedController, InlinedTypeSection } from './emit.js';
|
|
11
|
+
export { CLIENT_INDEX_FILE_NAME, JOBS_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitJobModule, emitJobsIndexModule, emitScriptsModule, jobModuleFileName } from './emit.js';
|
|
12
|
+
export type { EmitClientIndexModuleOptions, EmitControllerModuleOptions, EmitScriptsModuleOptions, EmittedController, EmittedJob, EmittedJobRuns, InlinedTypeSection } from './emit.js';
|
|
10
13
|
export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
|
|
11
|
-
export type { ClientGenerationCheck, ClientGenerationPlan, ClientGenerationResult, GenerateClientOptions, PlannedController, PlannedFile } from './generate.js';
|
|
14
|
+
export type { ClientGenerationCheck, ClientGenerationPlan, ClientGenerationResult, GenerateClientOptions, PlannedController, PlannedFile, PlannedJob } from './generate.js';
|
|
12
15
|
export { createInMemoryFileSystemAdapter, createNodeFileSystemAdapter, toPosixPath } from './file-system.js';
|
|
13
16
|
export type { FileSystemAdapter, InMemoryFileSystemAdapter } from './file-system.js';
|
|
14
17
|
export { CLI_USAGE, EXIT_PROBLEMS, EXIT_SUCCESS, EXIT_USAGE, runCli } from './cli/main.js';
|
package/dist-tooling/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** The generator as a library: what the `netsuite-api` command runs, for a build tool or a test to call directly. */
|
|
2
2
|
export { DEFAULT_CONFIG_FILE_NAME, ClientGeneratorConfigError, defaultClientGeneratorConfig, loadClientGeneratorConfig } from './config.js';
|
|
3
|
+
export { JOB_RUN_FIELD_SUFFIXES, netsuiteValueTypeNames, resolveJobRunFieldIds } from './config.js';
|
|
3
4
|
export { GENERATED_CLIENT_NAME, GENERATED_ENDPOINTS_TYPE_NAME, RAW_RESPONSE_TYPE_NAME, isControllerFileName, readControllerContract, readLeadingJsDoc } from './controllerReader.js';
|
|
5
|
+
export { GENERATED_JOB_RESULT_TYPE_NAME, JOB_SCRIPT_TYPE_HEADER, JOB_STAGE_NAMES, isJobFileName, readJobContract } from './jobReader.js';
|
|
4
6
|
export { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
|
|
5
|
-
export { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitScriptsModule } from './emit.js';
|
|
7
|
+
export { CLIENT_INDEX_FILE_NAME, JOBS_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitJobModule, emitJobsIndexModule, emitScriptsModule, jobModuleFileName } from './emit.js';
|
|
6
8
|
export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
|
|
7
9
|
export { createInMemoryFileSystemAdapter, createNodeFileSystemAdapter, toPosixPath } from './file-system.js';
|
|
8
10
|
export { CLI_USAGE, EXIT_PROBLEMS, EXIT_SUCCESS, EXIT_USAGE, runCli } from './cli/main.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CarriedTypeImport, ControllerProblem, InlinedTypeImport, ReadControllerOptions, TypeDeclaration } from './controllerReader.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads what a job run carries, from its source alone: the script it declares in its defineJob call,
|
|
4
|
+
* the stages it has, and the types on either end of a run. A run's input is the first parameter of
|
|
5
|
+
* getInputData and its result is what summarize returns, so those two annotations are the contract,
|
|
6
|
+
* the way a handler's parameter and return type are a controller's. A parse, not a type check: the
|
|
7
|
+
* ids are string literals and the shapes are written out.
|
|
8
|
+
*/
|
|
9
|
+
/** The stage names a job may declare, in the order NetSuite calls them. */
|
|
10
|
+
export declare const JOB_STAGE_NAMES: readonly ["getInputData", "map", "reduce", "summarize"];
|
|
11
|
+
export type JobStageName = (typeof JOB_STAGE_NAMES)[number];
|
|
12
|
+
/** The script type a job's leading JSDoc must declare. */
|
|
13
|
+
export declare const JOB_SCRIPT_TYPE_HEADER = "MapReduceScript";
|
|
14
|
+
/** The type the generated job module gives the run's input, and the one it gives the result. */
|
|
15
|
+
export declare const GENERATED_JOB_RESULT_TYPE_NAME = "Result";
|
|
16
|
+
declare const netsuiteValueTypes: readonly ["text", "integer", "decimal", "checkbox", "date", "select"];
|
|
17
|
+
export interface JobParameterContract {
|
|
18
|
+
/** The name the stages read it by. */
|
|
19
|
+
name: string;
|
|
20
|
+
id: string;
|
|
21
|
+
type: (typeof netsuiteValueTypes)[number];
|
|
22
|
+
}
|
|
23
|
+
/** The script a job declares, as read off its defineJob call. */
|
|
24
|
+
export interface DeclaredJob {
|
|
25
|
+
scriptId: string;
|
|
26
|
+
deployments: string[];
|
|
27
|
+
runParameter: string;
|
|
28
|
+
parameters: JobParameterContract[];
|
|
29
|
+
}
|
|
30
|
+
export interface JobContract {
|
|
31
|
+
/** The job's name: `closeStaleOrders` for closeStaleOrders.ts, matching `name` in its declaration. */
|
|
32
|
+
name: string;
|
|
33
|
+
filePath: string;
|
|
34
|
+
script: DeclaredJob;
|
|
35
|
+
carriedTypeImports: CarriedTypeImport[];
|
|
36
|
+
inlinedTypeImports: InlinedTypeImport[];
|
|
37
|
+
typeDeclarations: TypeDeclaration[];
|
|
38
|
+
/** The type of getInputData's first parameter, or undefined when a run takes no input. */
|
|
39
|
+
inputType?: string;
|
|
40
|
+
/** What summarize returns, or undefined when the job has no summarize stage of its own. */
|
|
41
|
+
resultType?: string;
|
|
42
|
+
/** The stages the job declares. */
|
|
43
|
+
stages: JobStageName[];
|
|
44
|
+
/** The stages the file exports as NetSuite entry points. */
|
|
45
|
+
exportedStages: string[];
|
|
46
|
+
}
|
|
47
|
+
export interface JobReadResult {
|
|
48
|
+
contract?: JobContract;
|
|
49
|
+
problems: ControllerProblem[];
|
|
50
|
+
}
|
|
51
|
+
export declare function isJobFileName(fileName: string): boolean;
|
|
52
|
+
export declare function readJobContract(filePath: string, source: string, options: ReadControllerOptions): JobReadResult;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,260 @@
|
|
|
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, from its source alone: the script it declares in its defineJob call,
|
|
7
|
+
* the stages it has, and the types on either end of a run. A run's input is the first parameter of
|
|
8
|
+
* getInputData and its result is what summarize returns, so those two annotations are the contract,
|
|
9
|
+
* the way a handler's parameter and return type are a controller's. A parse, not a type check: the
|
|
10
|
+
* ids are string literals and the shapes are written out.
|
|
11
|
+
*/
|
|
12
|
+
/** The stage names a job may declare, in the order NetSuite calls them. */
|
|
13
|
+
export const JOB_STAGE_NAMES = ['getInputData', 'map', 'reduce', 'summarize'];
|
|
14
|
+
/** The script type a job's leading JSDoc must declare. */
|
|
15
|
+
export const JOB_SCRIPT_TYPE_HEADER = 'MapReduceScript';
|
|
16
|
+
/** The type the generated job module gives the run's input, and the one it gives the result. */
|
|
17
|
+
export const GENERATED_JOB_RESULT_TYPE_NAME = 'Result';
|
|
18
|
+
const jobFileNamePattern = /^([a-z][A-Za-z0-9]*)\.ts$/;
|
|
19
|
+
const netsuiteValueTypes = ['text', 'integer', 'decimal', 'checkbox', 'date', 'select'];
|
|
20
|
+
export function isJobFileName(fileName) {
|
|
21
|
+
return jobFileNamePattern.test(fileName) && !fileName.endsWith('.d.ts');
|
|
22
|
+
}
|
|
23
|
+
/** `export const { getInputData, map, summarize } = defineJob(...)`: the call, and the names it exports. */
|
|
24
|
+
function findDefineJobCall(statement) {
|
|
25
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
26
|
+
const initializer = declaration.initializer;
|
|
27
|
+
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || initializer.expression.text !== 'defineJob')
|
|
28
|
+
continue;
|
|
29
|
+
if (!ts.isObjectBindingPattern(declaration.name))
|
|
30
|
+
return { call: initializer, exportedStages: [], destructured: false };
|
|
31
|
+
const exportedStages = declaration.name.elements
|
|
32
|
+
.map((element) => (ts.isIdentifier(element.name) ? element.name.text : undefined))
|
|
33
|
+
.filter((exported) => exported !== undefined);
|
|
34
|
+
return { call: initializer, exportedStages, destructured: true };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
function readStringArrayProperty(literal, propertyName) {
|
|
39
|
+
for (const property of literal.properties) {
|
|
40
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || property.name.text !== propertyName)
|
|
41
|
+
continue;
|
|
42
|
+
if (!ts.isArrayLiteralExpression(property.initializer))
|
|
43
|
+
return undefined;
|
|
44
|
+
const values = property.initializer.elements.map((element) => (ts.isStringLiteral(element) ? element.text : undefined));
|
|
45
|
+
return values.every((value) => value !== undefined) ? values : undefined;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function hasProperty(literal, propertyName) {
|
|
50
|
+
return literal.properties.some((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === propertyName);
|
|
51
|
+
}
|
|
52
|
+
function readObjectProperty(literal, propertyName) {
|
|
53
|
+
for (const property of literal.properties) {
|
|
54
|
+
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === propertyName && ts.isObjectLiteralExpression(property.initializer)) {
|
|
55
|
+
return property.initializer;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function readParameters(declaration, filePath) {
|
|
61
|
+
const problems = [];
|
|
62
|
+
const parameters = [];
|
|
63
|
+
const literal = readObjectProperty(declaration, 'parameters');
|
|
64
|
+
if (!literal) {
|
|
65
|
+
if (hasProperty(declaration, 'parameters'))
|
|
66
|
+
problems.push({ filePath, message: "'parameters' must be an object literal of { id, type } written inline; the generator reads the ids from it." });
|
|
67
|
+
return { parameters, problems };
|
|
68
|
+
}
|
|
69
|
+
for (const property of literal.properties) {
|
|
70
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
71
|
+
problems.push({ filePath, message: 'every script parameter is named by a plain identifier and declared as { id, type }.' });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const name = property.name.text;
|
|
75
|
+
if (!ts.isObjectLiteralExpression(property.initializer)) {
|
|
76
|
+
problems.push({ filePath, message: `parameter '${name}' must be written as { id: 'custscript_...', type: '...' }.` });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const id = readStringProperty(property.initializer, 'id');
|
|
80
|
+
const type = readStringProperty(property.initializer, 'type');
|
|
81
|
+
if (id === undefined)
|
|
82
|
+
problems.push({ filePath, message: `parameter '${name}' needs 'id' as a string literal: the script parameter's id in NetSuite.` });
|
|
83
|
+
if (type === undefined || !netsuiteValueTypes.includes(type)) {
|
|
84
|
+
problems.push({ filePath, message: `parameter '${name}' needs 'type' as one of ${netsuiteValueTypes.map((value) => `'${value}'`).join(', ')}.` });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (id !== undefined)
|
|
88
|
+
parameters.push({ name, id, type: type });
|
|
89
|
+
}
|
|
90
|
+
return { parameters, problems };
|
|
91
|
+
}
|
|
92
|
+
function readJobDeclaration(declaration, jobName, filePath) {
|
|
93
|
+
const problems = [];
|
|
94
|
+
const name = readStringProperty(declaration, 'name');
|
|
95
|
+
const scriptId = readStringProperty(declaration, 'scriptId');
|
|
96
|
+
const runParameter = readStringProperty(declaration, 'runParameter');
|
|
97
|
+
const deployments = readStringArrayProperty(declaration, 'deployments');
|
|
98
|
+
if (name === undefined)
|
|
99
|
+
problems.push({ filePath, message: "the job declaration needs 'name' as a string literal; the generator reads it from the source." });
|
|
100
|
+
else if (name !== jobName)
|
|
101
|
+
problems.push({ filePath, message: `the job declaration says name: '${name}' but the file is ${jobName}.ts; the name is the file name.` });
|
|
102
|
+
if (scriptId === undefined)
|
|
103
|
+
problems.push({ filePath, message: "the job declaration needs 'scriptId' as a string literal." });
|
|
104
|
+
if (runParameter === undefined) {
|
|
105
|
+
problems.push({ filePath, message: "the job declaration needs 'runParameter' as a string literal: the script parameter the run id is passed in." });
|
|
106
|
+
}
|
|
107
|
+
if (deployments === undefined) {
|
|
108
|
+
problems.push({ filePath, message: "the job declaration needs 'deployments' as an array of string literals: every deployment a run may be started on." });
|
|
109
|
+
}
|
|
110
|
+
else if (deployments.length === 0) {
|
|
111
|
+
problems.push({ filePath, message: "'deployments' is empty; a job needs at least one deployment to run on." });
|
|
112
|
+
}
|
|
113
|
+
if (!hasProperty(declaration, 'runs')) {
|
|
114
|
+
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." });
|
|
115
|
+
}
|
|
116
|
+
const readParametersResult = readParameters(declaration, filePath);
|
|
117
|
+
problems.push(...readParametersResult.problems);
|
|
118
|
+
if (problems.length > 0 || scriptId === undefined || runParameter === undefined || deployments === undefined)
|
|
119
|
+
return { problems };
|
|
120
|
+
return { script: { scriptId, deployments, runParameter, parameters: readParametersResult.parameters }, problems };
|
|
121
|
+
}
|
|
122
|
+
function readStages(literal, filePath, sourceFile) {
|
|
123
|
+
const problems = [];
|
|
124
|
+
const stages = [];
|
|
125
|
+
let inputType;
|
|
126
|
+
let resultType;
|
|
127
|
+
for (const property of literal.properties) {
|
|
128
|
+
const stageName = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined;
|
|
129
|
+
if (stageName === undefined || !JOB_STAGE_NAMES.includes(stageName)) {
|
|
130
|
+
problems.push({ filePath, message: `'${stageName ?? 'a stage'}' is not a stage; a job declares ${JOB_STAGE_NAMES.join(', ')}.` });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
let handler;
|
|
134
|
+
if (ts.isMethodDeclaration(property))
|
|
135
|
+
handler = property;
|
|
136
|
+
else if (ts.isPropertyAssignment(property) && (ts.isArrowFunction(property.initializer) || ts.isFunctionExpression(property.initializer)))
|
|
137
|
+
handler = property.initializer;
|
|
138
|
+
if (!handler) {
|
|
139
|
+
problems.push({ filePath, message: `stage '${stageName}' must be an inline function; a reference to a function elsewhere carries no types the generator can read.` });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
stages.push(stageName);
|
|
143
|
+
if (stageName === 'getInputData') {
|
|
144
|
+
const parameter = handler.parameters[0];
|
|
145
|
+
if (parameter && !parameter.type) {
|
|
146
|
+
problems.push({ filePath, message: "getInputData has no type on its first parameter; a run's input shape is read from it." });
|
|
147
|
+
}
|
|
148
|
+
inputType = parameter?.type?.getText(sourceFile);
|
|
149
|
+
}
|
|
150
|
+
if (stageName === 'summarize') {
|
|
151
|
+
if (!handler.type)
|
|
152
|
+
problems.push({ filePath, message: "summarize has no return type annotation; a run's result shape is read from it." });
|
|
153
|
+
resultType = handler.type?.getText(sourceFile);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!stages.includes('getInputData'))
|
|
157
|
+
problems.push({ filePath, message: 'a job declares getInputData; it is what NetSuite asks for the run\'s work.' });
|
|
158
|
+
if (!stages.includes('map') && !stages.includes('reduce')) {
|
|
159
|
+
problems.push({ filePath, message: 'a job declares a map stage, a reduce stage, or both; NetSuite has nothing to run otherwise.' });
|
|
160
|
+
}
|
|
161
|
+
return { stages, inputType, resultType, problems };
|
|
162
|
+
}
|
|
163
|
+
export function readJobContract(filePath, source, options) {
|
|
164
|
+
const problems = [];
|
|
165
|
+
const fileName = nodePath.basename(toPosixPath(filePath));
|
|
166
|
+
const nameMatch = jobFileNamePattern.exec(fileName);
|
|
167
|
+
if (!nameMatch)
|
|
168
|
+
return { problems: [{ filePath, message: 'a job file is named <name>.ts, with <name> in camelCase; nothing else lives in the jobs folder.' }] };
|
|
169
|
+
const name = nameMatch[1];
|
|
170
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
171
|
+
const header = readScriptTypeHeader(source);
|
|
172
|
+
if (header !== JOB_SCRIPT_TYPE_HEADER) {
|
|
173
|
+
problems.push({ filePath, message: `the leading JSDoc says '@NScriptType ${header ?? '(none)'}'; a job is a ${JOB_SCRIPT_TYPE_HEADER}.` });
|
|
174
|
+
}
|
|
175
|
+
const carriedTypeImports = [];
|
|
176
|
+
const inlinedTypeImports = [];
|
|
177
|
+
const typeDeclarations = [];
|
|
178
|
+
let contract;
|
|
179
|
+
for (const statement of sourceFile.statements) {
|
|
180
|
+
if (ts.isImportDeclaration(statement)) {
|
|
181
|
+
const result = readTypeImport(statement, filePath, options);
|
|
182
|
+
problems.push(...result.problems);
|
|
183
|
+
if (result.read?.kind === 'carried')
|
|
184
|
+
carriedTypeImports.push(result.read.typeImport);
|
|
185
|
+
else if (result.read?.kind === 'inlined')
|
|
186
|
+
inlinedTypeImports.push(result.read.typeImport);
|
|
187
|
+
else if (result.read?.kind === 'controller') {
|
|
188
|
+
problems.push({ filePath, message: 'a job does not take types from a controller; its input and result are its own, or a service\'s.' });
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {
|
|
193
|
+
if (!hasExportModifier(statement)) {
|
|
194
|
+
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.` });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (statement.name.text === GENERATED_JOB_RESULT_TYPE_NAME) {
|
|
198
|
+
problems.push({ filePath, message: `type '${statement.name.text}' is the name the generated module gives the run's result; call this shape something else.` });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const jsDoc = readLeadingJsDoc(statement, sourceFile);
|
|
202
|
+
typeDeclarations.push({ name: statement.name.text, text: `${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}` });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!ts.isVariableStatement(statement))
|
|
206
|
+
continue;
|
|
207
|
+
const found = findDefineJobCall(statement);
|
|
208
|
+
if (!found)
|
|
209
|
+
continue;
|
|
210
|
+
if (contract) {
|
|
211
|
+
problems.push({ filePath, message: 'a job file has one defineJob call; a second one is a second script.' });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!hasExportModifier(statement) || !found.destructured) {
|
|
215
|
+
problems.push({ filePath, message: "the stages are exported from the call: `export const { getInputData, map, summarize } = defineJob({ ... }, { ... })`; NetSuite looks for those exports." });
|
|
216
|
+
}
|
|
217
|
+
const [declarationArgument, stagesArgument] = found.call.arguments;
|
|
218
|
+
if (!declarationArgument || !ts.isObjectLiteralExpression(declarationArgument)) {
|
|
219
|
+
problems.push({ filePath, message: 'defineJob takes the job declaration as an object literal written inline.' });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!stagesArgument || !ts.isObjectLiteralExpression(stagesArgument)) {
|
|
223
|
+
problems.push({ filePath, message: 'defineJob takes the stages as an object literal written inline, each stage a function with its types annotated.' });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const declared = readJobDeclaration(declarationArgument, name, filePath);
|
|
227
|
+
problems.push(...declared.problems);
|
|
228
|
+
const stages = readStages(stagesArgument, filePath, sourceFile);
|
|
229
|
+
problems.push(...stages.problems);
|
|
230
|
+
for (const exported of found.exportedStages) {
|
|
231
|
+
if (!JOB_STAGE_NAMES.includes(exported)) {
|
|
232
|
+
problems.push({ filePath, message: `'${exported}' is not a stage, so NetSuite has no entry point by that name; export ${JOB_STAGE_NAMES.join(', ')}.` });
|
|
233
|
+
}
|
|
234
|
+
else if (exported !== 'summarize' && !stages.stages.includes(exported)) {
|
|
235
|
+
problems.push({ filePath, message: `'${exported}' is exported but not declared; a stage exported without a declaration throws when NetSuite calls it.` });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const stage of stages.stages) {
|
|
239
|
+
if (!found.exportedStages.includes(stage)) {
|
|
240
|
+
problems.push({ filePath, message: `stage '${stage}' is declared but not exported; NetSuite only runs the stages the file exports.` });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// The run is closed in summarize, so a job that leaves it out would leave every run of it looking unfinished.
|
|
244
|
+
if (!found.exportedStages.includes('summarize')) {
|
|
245
|
+
problems.push({ filePath, message: 'every job exports summarize, declared or not: it is where the run is closed and its result written.' });
|
|
246
|
+
}
|
|
247
|
+
if (!declared.script)
|
|
248
|
+
continue;
|
|
249
|
+
contract = { name, filePath, script: declared.script, inputType: stages.inputType, resultType: stages.resultType, stages: stages.stages, exportedStages: found.exportedStages };
|
|
250
|
+
}
|
|
251
|
+
if (!contract) {
|
|
252
|
+
if (problems.length === 0) {
|
|
253
|
+
problems.push({ filePath, message: 'must declare its script: `export const { getInputData, map, summarize } = defineJob({ name, scriptId, deployments, runParameter, runs }, { ... })`.' });
|
|
254
|
+
}
|
|
255
|
+
return { problems };
|
|
256
|
+
}
|
|
257
|
+
if (problems.length > 0)
|
|
258
|
+
return { problems };
|
|
259
|
+
return { contract: { ...contract, carriedTypeImports, inlinedTypeImports, typeDeclarations }, problems };
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amerilux/netsuite-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|