@amerilux/netsuite-api 0.5.0 → 0.6.1
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 +34 -15
- package/dist-tooling/config.js +7 -1
- package/dist-tooling/file-system.d.ts +2 -0
- package/dist-tooling/file-system.js +23 -0
- package/dist-tooling/generate.js +50 -20
- package/dist-tooling/index.d.ts +1 -1
- package/dist-tooling/index.js +1 -1
- package/dist-tooling/jobReader.d.ts +38 -7
- package/dist-tooling/jobReader.js +194 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,11 +121,16 @@ A hook imports `{ customer }` from it, calls `customer.api.search({ search: 'acm
|
|
|
121
121
|
"clientModule": "@amerilux/netsuite-api/client",
|
|
122
122
|
"wireModule": "@amerilux/netsuite-api",
|
|
123
123
|
"typeImports": { "@amerilux/netsuite-api/server": "@amerilux/netsuite-api/client" },
|
|
124
|
-
"inlineTypes": {
|
|
124
|
+
"inlineTypes": {
|
|
125
|
+
"../types/models.gen": "api/src/types/models.gen.ts",
|
|
126
|
+
"../services/*": "api/src/services/*.ts",
|
|
127
|
+
"../../types/models.gen": "api/src/types/models.gen.ts",
|
|
128
|
+
"../../services/*": "api/src/services/*.ts"
|
|
129
|
+
}
|
|
125
130
|
}
|
|
126
131
|
```
|
|
127
132
|
|
|
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.
|
|
133
|
+
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. The `../../` entries are the same two files named from one folder deeper, because a job is a folder and its stage files sit inside it. `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.
|
|
129
134
|
|
|
130
135
|
## The client at runtime
|
|
131
136
|
|
|
@@ -195,19 +200,21 @@ export const listRolesForEmployee = (employeeId: number) => userRolesApi.byEmplo
|
|
|
195
200
|
|
|
196
201
|
## Jobs
|
|
197
202
|
|
|
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.
|
|
203
|
+
A job is a Map/Reduce script written as stages, and it is a folder: `api/src/jobs/<name>/<name>.ts` declares it and the stages it is made of sit beside it, one file each. 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.
|
|
204
|
+
|
|
205
|
+
The definition file is a declaration and a wiring, so what a job is reads at a glance: its ids, its deployments, its parameters, and which stages it has.
|
|
199
206
|
|
|
200
207
|
```ts
|
|
208
|
+
// api/src/jobs/closeStaleOrders/closeStaleOrders.ts
|
|
201
209
|
/**
|
|
202
210
|
* @NApiVersion 2.1
|
|
203
211
|
* @NScriptType MapReduceScript
|
|
204
212
|
*/
|
|
205
213
|
import { defineJob } from '@amerilux/netsuite-api/server';
|
|
206
|
-
import { jobRuns } from '
|
|
207
|
-
import {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
export interface CloseStaleResult { closed: number }
|
|
214
|
+
import { jobRuns } from '../../scripts.gen';
|
|
215
|
+
import { getInputDataFunction } from './getInputData';
|
|
216
|
+
import { mapFunction } from './map';
|
|
217
|
+
import { summarizeFunction } from './summarize';
|
|
211
218
|
|
|
212
219
|
export const { getInputData, map, summarize } = defineJob({
|
|
213
220
|
name: 'closeStaleOrders',
|
|
@@ -217,21 +224,33 @@ export const { getInputData, map, summarize } = defineJob({
|
|
|
217
224
|
parameters: { batchSize: { id: 'custscript_app_close_stale_batch', type: 'integer' } },
|
|
218
225
|
runs: jobRuns,
|
|
219
226
|
}, {
|
|
220
|
-
getInputData:
|
|
221
|
-
map:
|
|
222
|
-
|
|
223
|
-
},
|
|
224
|
-
summarize: (summary): CloseStaleResult => ({ closed: summary.output.length }),
|
|
227
|
+
getInputData: getInputDataFunction,
|
|
228
|
+
map: mapFunction,
|
|
229
|
+
summarize: summarizeFunction,
|
|
225
230
|
});
|
|
226
231
|
```
|
|
227
232
|
|
|
228
|
-
|
|
233
|
+
Each stage is then a file named after the stage NetSuite calls, holding the work and the shapes on its own boundary — open map.ts to see what the map stage does:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// api/src/jobs/closeStaleOrders/getInputData.ts
|
|
237
|
+
import { listStaleOrders, type StaleOrder } from '../../services/staleOrderService';
|
|
238
|
+
|
|
239
|
+
/** What a run of this job is asked to do. */
|
|
240
|
+
export interface CloseStaleRequest { olderThanDays: number }
|
|
241
|
+
|
|
242
|
+
export const getInputDataFunction = (input: CloseStaleRequest): StaleOrder[] => listStaleOrders(input.olderThanDays);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A stage file exports the stage's name plus `Function`, because the definition file exports the plain names NetSuite looks for and the two would collide. A stage file may name a shape another file of the job's folder declares — the value a map stage writes is declared in map.ts and named again in summarize.ts — and the generator reads it there. A stage may also be written inline in the definition file, which suits a job whose stages are a line each — the cleanup job `npm run add:jobs` writes is one.
|
|
246
|
+
|
|
247
|
+
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 wherever the stage is written, the way it reads an endpoint's request and response, and copies the shapes the result names into the browser's module whether the stage file declares them or takes them from a service. 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
248
|
|
|
230
249
|
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
250
|
|
|
232
251
|
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
252
|
|
|
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
|
|
253
|
+
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 passes the job and the input. In the folder shape that decision belongs in the job's own folder (`start.ts`), so everything about a job is one place and a controller reaches for it by name.
|
|
235
254
|
|
|
236
255
|
```ts
|
|
237
256
|
// api/src/repositories/jobRunRepository.ts
|
package/dist-tooling/config.js
CHANGED
|
@@ -27,7 +27,13 @@ export const defaultClientGeneratorConfig = {
|
|
|
27
27
|
clientModule: '@amerilux/netsuite-api/client',
|
|
28
28
|
wireModule: '@amerilux/netsuite-api',
|
|
29
29
|
typeImports: { '@amerilux/netsuite-api/server': '@amerilux/netsuite-api/client' },
|
|
30
|
-
inlineTypes: {
|
|
30
|
+
inlineTypes: {
|
|
31
|
+
'../types/models.gen': 'api/src/types/models.gen.ts',
|
|
32
|
+
'../services/*': 'api/src/services/*.ts',
|
|
33
|
+
// A job is a folder, so its definition and its stage files name the same places from one level deeper.
|
|
34
|
+
'../../types/models.gen': 'api/src/types/models.gen.ts',
|
|
35
|
+
'../../services/*': 'api/src/services/*.ts',
|
|
36
|
+
},
|
|
31
37
|
};
|
|
32
38
|
const wildcardInlineTypesKeyPattern = /^([^*]*)\*([^*]*)$/;
|
|
33
39
|
const wildcardSegmentPattern = /^[A-Za-z0-9_-]+$/;
|
|
@@ -8,6 +8,8 @@ export interface FileSystemAdapter {
|
|
|
8
8
|
ensureDirectory(directoryPath: string): void;
|
|
9
9
|
/** Lists the absolute paths of the files directly inside a directory. Returns [] when it does not exist. */
|
|
10
10
|
listFiles(directoryPath: string): string[];
|
|
11
|
+
/** Lists the absolute paths of the directories directly inside a directory: one per job, under the jobs folder. */
|
|
12
|
+
listDirectories(directoryPath: string): string[];
|
|
11
13
|
}
|
|
12
14
|
export declare function toPosixPath(filePath: string): string;
|
|
13
15
|
export declare function createNodeFileSystemAdapter(): FileSystemAdapter;
|
|
@@ -19,6 +19,15 @@ export function createNodeFileSystemAdapter() {
|
|
|
19
19
|
.map((entry) => nodePath.join(directoryPath, entry.name))
|
|
20
20
|
.sort();
|
|
21
21
|
},
|
|
22
|
+
listDirectories: (directoryPath) => {
|
|
23
|
+
if (!nodeFileSystem.existsSync(directoryPath) || !nodeFileSystem.statSync(directoryPath).isDirectory())
|
|
24
|
+
return [];
|
|
25
|
+
return nodeFileSystem
|
|
26
|
+
.readdirSync(directoryPath, { withFileTypes: true })
|
|
27
|
+
.filter((entry) => entry.isDirectory())
|
|
28
|
+
.map((entry) => nodePath.join(directoryPath, entry.name))
|
|
29
|
+
.sort();
|
|
30
|
+
},
|
|
22
31
|
};
|
|
23
32
|
}
|
|
24
33
|
function normalizeKey(filePath) {
|
|
@@ -53,5 +62,19 @@ export function createInMemoryFileSystemAdapter(initialFiles = {}) {
|
|
|
53
62
|
.filter((filePath) => filePath.startsWith(prefix) && !filePath.slice(prefix.length).includes('/'))
|
|
54
63
|
.sort();
|
|
55
64
|
},
|
|
65
|
+
listDirectories: (directoryPath) => {
|
|
66
|
+
const prefix = `${normalizeKey(directoryPath)}/`;
|
|
67
|
+
const names = new Set();
|
|
68
|
+
for (const filePath of [...files.keys(), ...directories]) {
|
|
69
|
+
if (!filePath.startsWith(prefix))
|
|
70
|
+
continue;
|
|
71
|
+
const [name, ...rest] = filePath.slice(prefix.length).split('/');
|
|
72
|
+
if (name !== undefined && rest.length > 0)
|
|
73
|
+
names.add(name);
|
|
74
|
+
}
|
|
75
|
+
return Array.from(names)
|
|
76
|
+
.sort()
|
|
77
|
+
.map((name) => `${prefix}${name}`);
|
|
78
|
+
},
|
|
56
79
|
};
|
|
57
80
|
}
|
package/dist-tooling/generate.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as nodePath from 'node:path';
|
|
|
2
2
|
import { resolveInlineTypesFile, resolveJobRunFieldIds } from './config.js';
|
|
3
3
|
import { isControllerFileName, readControllerContract } from './controllerReader.js';
|
|
4
4
|
import { CLIENT_INDEX_FILE_NAME, JOBS_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitJobModule, emitJobsIndexModule, emitScriptsModule, jobModuleFileName, sortControllers, sortJobs, } from './emit.js';
|
|
5
|
-
import {
|
|
5
|
+
import { jobDefinitionFileName, readJobContract, readJobFolderName } from './jobReader.js';
|
|
6
6
|
import { toPosixPath } from './file-system.js';
|
|
7
7
|
import { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
|
|
8
8
|
import { readReferencedNames } from './wireTypes.js';
|
|
@@ -169,16 +169,52 @@ export function planClientGeneration({ config, fileSystem }) {
|
|
|
169
169
|
problems.push(...findDatesInRequestShapes(contract, inlinedTypes, controllerLabel));
|
|
170
170
|
emitted.push({ contract, sourceLabel: controllerLabel, inlinedTypes });
|
|
171
171
|
}
|
|
172
|
-
// Jobs: the same reading, with a run instead of a request and a record instead of a reply.
|
|
172
|
+
// Jobs: the same reading, with a run instead of a request and a record instead of a reply. A job is a
|
|
173
|
+
// folder — <name>/<name>.ts declares it and its stage files sit beside it — so the shapes on either end
|
|
174
|
+
// of a run are read wherever the stage that names them lives.
|
|
173
175
|
const jobsDirectory = resolve(config.jobs);
|
|
174
176
|
const emittedJobs = [];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
/** Copies what one type import names into the job's sections, following imports between the files it reaches. */
|
|
178
|
+
function addInlinedTypes(sections, typeImport, fromLabel) {
|
|
179
|
+
const file = readInlinable(typeImport.specifier);
|
|
180
|
+
if (!file || file === 'missing')
|
|
181
|
+
return;
|
|
182
|
+
const selected = selectInlinedTypes(file, typeImport.names, fromLabel, readInlinable);
|
|
183
|
+
problems.push(...selected.problems);
|
|
184
|
+
for (const selectedSection of selected.sections)
|
|
185
|
+
addDeclarations(sections, selectedSection.filePath, selectedSection.declarations);
|
|
186
|
+
}
|
|
187
|
+
/** One section per source file, so a shape copied twice is written once. */
|
|
188
|
+
function addDeclarations(sections, sourceLabel, declarations) {
|
|
189
|
+
const section = sections.find((existing) => existing.sourceLabel === sourceLabel);
|
|
190
|
+
if (section)
|
|
191
|
+
section.declarations.push(...declarations.filter((declaration) => !section.declarations.some((existing) => existing.name === declaration.name)));
|
|
192
|
+
else
|
|
193
|
+
sections.push({ sourceLabel, declarations: [...declarations] });
|
|
194
|
+
}
|
|
195
|
+
for (const strayPath of fileSystem.listFiles(jobsDirectory)) {
|
|
196
|
+
problems.push({ filePath: label(strayPath), message: 'a job lives in a folder of its own, declared by the file of that name; nothing sits in the jobs folder itself.' });
|
|
197
|
+
}
|
|
198
|
+
for (const jobDirectory of fileSystem.listDirectories(jobsDirectory)) {
|
|
199
|
+
const jobName = readJobFolderName(nodePath.basename(jobDirectory));
|
|
200
|
+
if (jobName === undefined) {
|
|
201
|
+
problems.push({ filePath: label(jobDirectory), message: 'a job folder is named for its job, in camelCase.' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const definitionPath = nodePath.join(jobDirectory, jobDefinitionFileName(jobName));
|
|
205
|
+
if (!fileSystem.fileExists(definitionPath)) {
|
|
206
|
+
problems.push({ filePath: label(jobDirectory), message: `there is no ${jobDefinitionFileName(jobName)} here; the file of the folder's own name is what declares the job.` });
|
|
179
207
|
continue;
|
|
180
208
|
}
|
|
181
|
-
const
|
|
209
|
+
const jobLabel = label(definitionPath);
|
|
210
|
+
const result = readJobContract(jobLabel, fileSystem.readTextFile(definitionPath), {
|
|
211
|
+
typeImports: config.typeImports,
|
|
212
|
+
inlineTypes: config.inlineTypes,
|
|
213
|
+
readStageFile: (specifier) => {
|
|
214
|
+
const stagePath = `${nodePath.resolve(jobDirectory, specifier)}.ts`;
|
|
215
|
+
return fileSystem.fileExists(stagePath) ? { filePath: label(stagePath), source: fileSystem.readTextFile(stagePath) } : undefined;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
182
218
|
problems.push(...result.problems);
|
|
183
219
|
const contract = result.contract;
|
|
184
220
|
if (!contract)
|
|
@@ -187,20 +223,14 @@ export function planClientGeneration({ config, fileSystem }) {
|
|
|
187
223
|
problems.push({ filePath: jobLabel, message: `a controller is named '${contract.name}' too; their generated modules would be the same file.` });
|
|
188
224
|
}
|
|
189
225
|
const inlinedTypes = [];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
problems.push(...selected.problems);
|
|
196
|
-
for (const selectedSection of selected.sections) {
|
|
197
|
-
const section = inlinedTypes.find((existing) => existing.sourceLabel === selectedSection.filePath);
|
|
198
|
-
if (section)
|
|
199
|
-
section.declarations.push(...selectedSection.declarations.filter((declaration) => !section.declarations.some((existing) => existing.name === declaration.name)));
|
|
200
|
-
else
|
|
201
|
-
inlinedTypes.push({ sourceLabel: selectedSection.filePath, declarations: selectedSection.declarations });
|
|
202
|
-
}
|
|
226
|
+
// What the job's own files declare, then whatever they take from a service: both are the run's shapes.
|
|
227
|
+
for (const folderFile of contract.folderFiles) {
|
|
228
|
+
addDeclarations(inlinedTypes, folderFile.filePath, folderFile.declarations);
|
|
229
|
+
for (const typeImport of folderFile.inlinedTypeImports)
|
|
230
|
+
addInlinedTypes(inlinedTypes, typeImport, folderFile.filePath);
|
|
203
231
|
}
|
|
232
|
+
for (const typeImport of contract.inlinedTypeImports)
|
|
233
|
+
addInlinedTypes(inlinedTypes, typeImport, jobLabel);
|
|
204
234
|
problems.push(...findDatesInJobInput(contract, inlinedTypes, jobLabel));
|
|
205
235
|
emittedJobs.push({ contract, sourceLabel: jobLabel, inlinedTypes });
|
|
206
236
|
}
|
package/dist-tooling/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { JOB_RUN_FIELD_SUFFIXES, netsuiteValueTypeNames, resolveJobRunFieldIds }
|
|
|
4
4
|
export type { ClientGeneratorConfig, JobRunFieldName, JobRunsSettings, NetsuiteValueTypeName, ResolvedClientGeneratorConfig } from './config.js';
|
|
5
5
|
export { GENERATED_CLIENT_NAME, GENERATED_ENDPOINTS_TYPE_NAME, RAW_RESPONSE_TYPE_NAME, isControllerFileName, readControllerContract, readLeadingJsDoc } from './controllerReader.js';
|
|
6
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,
|
|
7
|
+
export { GENERATED_JOB_RESULT_TYPE_NAME, JOB_SCRIPT_TYPE_HEADER, JOB_STAGE_NAMES, jobDefinitionFileName, readJobContract, readJobFolderName } from './jobReader.js';
|
|
8
8
|
export type { DeclaredJob, JobContract, JobParameterContract, JobReadResult, JobStageName } from './jobReader.js';
|
|
9
9
|
export { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
|
|
10
10
|
export type { InlinableTypeDeclaration, InlinableTypesFile, SelectedInlinedTypes } from './typesFileReader.js';
|
package/dist-tooling/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { DEFAULT_CONFIG_FILE_NAME, ClientGeneratorConfigError, defaultClientGeneratorConfig, loadClientGeneratorConfig } from './config.js';
|
|
3
3
|
export { JOB_RUN_FIELD_SUFFIXES, netsuiteValueTypeNames, resolveJobRunFieldIds } from './config.js';
|
|
4
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,
|
|
5
|
+
export { GENERATED_JOB_RESULT_TYPE_NAME, JOB_SCRIPT_TYPE_HEADER, JOB_STAGE_NAMES, jobDefinitionFileName, readJobContract, readJobFolderName } from './jobReader.js';
|
|
6
6
|
export { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
|
|
7
7
|
export { CLIENT_INDEX_FILE_NAME, JOBS_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitJobModule, emitJobsIndexModule, emitScriptsModule, jobModuleFileName } from './emit.js';
|
|
8
8
|
export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { CarriedTypeImport, ControllerProblem, InlinedTypeImport, ReadControllerOptions, TypeDeclaration } from './controllerReader.js';
|
|
2
2
|
/**
|
|
3
|
-
* Reads what a job run carries
|
|
4
|
-
* the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Reads what a job run carries: the script it declares in its defineJob call, the stages it has, and
|
|
4
|
+
* the types on either end of a run. A run's input is the first parameter of getInputData and its result
|
|
5
|
+
* is what summarize returns, so those two annotations are the contract, the way a handler's parameter
|
|
6
|
+
* and return type are a controller's. A parse, not a type check: the ids are string literals and the
|
|
7
|
+
* shapes are written out.
|
|
8
|
+
*
|
|
9
|
+
* A job is a folder — `<name>/<name>.ts` declares it — and a stage is either a function written inline
|
|
10
|
+
* or one imported from a file beside it (`./map`), which is where the annotations are then read from. So
|
|
11
|
+
* a developer opens map.ts to see what the map stage does, and the definition file stays a declaration.
|
|
8
12
|
*/
|
|
9
13
|
/** The stage names a job may declare, in the order NetSuite calls them. */
|
|
10
14
|
export declare const JOB_STAGE_NAMES: readonly ["getInputData", "map", "reduce", "summarize"];
|
|
@@ -27,6 +31,28 @@ export interface DeclaredJob {
|
|
|
27
31
|
runParameter: string;
|
|
28
32
|
parameters: JobParameterContract[];
|
|
29
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A file of the job's own folder the reader had to read: a stage, or a file a stage takes a shape from. What
|
|
36
|
+
* it declares is copied into the browser's module like a service's, because a run's shapes are written where
|
|
37
|
+
* its stages are — the value a map stage writes is declared in map.ts and named again in summarize.ts.
|
|
38
|
+
*/
|
|
39
|
+
export interface JobFolderFile {
|
|
40
|
+
filePath: string;
|
|
41
|
+
declarations: TypeDeclaration[];
|
|
42
|
+
inlinedTypeImports: InlinedTypeImport[];
|
|
43
|
+
carriedTypeImports: CarriedTypeImport[];
|
|
44
|
+
}
|
|
45
|
+
/** What a job's reader needs beyond a controller's: the files its stages are imported from. */
|
|
46
|
+
export interface ReadJobOptions extends ReadControllerOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Reads a file a stage is imported from, the specifier resolved against the job's own folder, or
|
|
49
|
+
* undefined when there is no such file. Without it, only inline stages can be read.
|
|
50
|
+
*/
|
|
51
|
+
readStageFile?(specifier: string): {
|
|
52
|
+
filePath: string;
|
|
53
|
+
source: string;
|
|
54
|
+
} | undefined;
|
|
55
|
+
}
|
|
30
56
|
export interface JobContract {
|
|
31
57
|
/** The job's name: `closeStaleOrders` for closeStaleOrders.ts, matching `name` in its declaration. */
|
|
32
58
|
name: string;
|
|
@@ -43,11 +69,16 @@ export interface JobContract {
|
|
|
43
69
|
stages: JobStageName[];
|
|
44
70
|
/** The stages the file exports as NetSuite entry points. */
|
|
45
71
|
exportedStages: string[];
|
|
72
|
+
/** The files of the job's folder that were read, in that order: its stages, and whatever they name. */
|
|
73
|
+
folderFiles: JobFolderFile[];
|
|
46
74
|
}
|
|
47
75
|
export interface JobReadResult {
|
|
48
76
|
contract?: JobContract;
|
|
49
77
|
problems: ControllerProblem[];
|
|
50
78
|
}
|
|
51
|
-
|
|
52
|
-
export declare function
|
|
79
|
+
/** The name a job folder declares: `approveOrders` for api/src/jobs/approveOrders, or undefined when it is not a name. */
|
|
80
|
+
export declare function readJobFolderName(folderName: string): string | undefined;
|
|
81
|
+
/** 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. */
|
|
82
|
+
export declare function jobDefinitionFileName(jobName: string): string;
|
|
83
|
+
export declare function readJobContract(filePath: string, source: string, options: ReadJobOptions): JobReadResult;
|
|
53
84
|
export {};
|
|
@@ -3,11 +3,15 @@ import ts from 'typescript';
|
|
|
3
3
|
import { hasExportModifier, readLeadingJsDoc, readScriptTypeHeader, readStringProperty, readTypeImport } from './controllerReader.js';
|
|
4
4
|
import { toPosixPath } from './file-system.js';
|
|
5
5
|
/**
|
|
6
|
-
* Reads what a job run carries
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
15
|
*/
|
|
12
16
|
/** The stage names a job may declare, in the order NetSuite calls them. */
|
|
13
17
|
export const JOB_STAGE_NAMES = ['getInputData', 'map', 'reduce', 'summarize'];
|
|
@@ -16,9 +20,15 @@ export const JOB_SCRIPT_TYPE_HEADER = 'MapReduceScript';
|
|
|
16
20
|
/** The type the generated job module gives the run's input, and the one it gives the result. */
|
|
17
21
|
export const GENERATED_JOB_RESULT_TYPE_NAME = 'Result';
|
|
18
22
|
const jobFileNamePattern = /^([a-z][A-Za-z0-9]*)\.ts$/;
|
|
23
|
+
const jobFolderNamePattern = /^[a-z][A-Za-z0-9]*$/;
|
|
19
24
|
const netsuiteValueTypes = ['text', 'integer', 'decimal', 'checkbox', 'date', 'select'];
|
|
20
|
-
|
|
21
|
-
|
|
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`;
|
|
22
32
|
}
|
|
23
33
|
/** `export const { getInputData, map, summarize } = defineJob(...)`: the call, and the names it exports. */
|
|
24
34
|
function findDefineJobCall(statement) {
|
|
@@ -98,7 +108,7 @@ function readJobDeclaration(declaration, jobName, filePath) {
|
|
|
98
108
|
if (name === undefined)
|
|
99
109
|
problems.push({ filePath, message: "the job declaration needs 'name' as a string literal; the generator reads it from the source." });
|
|
100
110
|
else if (name !== jobName)
|
|
101
|
-
problems.push({ filePath, message: `the job declaration says name: '${name}' but the
|
|
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.` });
|
|
102
112
|
if (scriptId === undefined)
|
|
103
113
|
problems.push({ filePath, message: "the job declaration needs 'scriptId' as a string literal." });
|
|
104
114
|
if (runParameter === undefined) {
|
|
@@ -119,38 +129,183 @@ function readJobDeclaration(declaration, jobName, filePath) {
|
|
|
119
129
|
return { problems };
|
|
120
130
|
return { script: { scriptId, deployments, runParameter, parameters: readParametersResult.parameters }, problems };
|
|
121
131
|
}
|
|
122
|
-
|
|
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, folderFiles) {
|
|
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
|
+
if (result.read?.kind === 'carried')
|
|
145
|
+
carriedTypeImports.push(result.read.typeImport);
|
|
146
|
+
else if (result.read?.kind === 'inlined')
|
|
147
|
+
inlinedTypeImports.push(result.read.typeImport);
|
|
148
|
+
else if (result.read?.kind === 'controller') {
|
|
149
|
+
problems.push({ filePath, message: "a job does not take types from a controller; its input and result are its own, or a service's." });
|
|
150
|
+
}
|
|
151
|
+
else if (result.problems.length > 0 && ts.isStringLiteral(statement.moduleSpecifier) && isRelativeSpecifier(statement.moduleSpecifier.text)) {
|
|
152
|
+
// A shape another file of this job's folder declares: read there instead of reported as unreachable.
|
|
153
|
+
const sibling = folderFiles?.read(statement.moduleSpecifier.text);
|
|
154
|
+
if (!sibling)
|
|
155
|
+
problems.push(...result.problems);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
problems.push(...result.problems);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement) && !ts.isEnumDeclaration(statement))
|
|
162
|
+
continue;
|
|
163
|
+
if (!hasExportModifier(statement)) {
|
|
164
|
+
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.` });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (statement.name.text === GENERATED_JOB_RESULT_TYPE_NAME) {
|
|
168
|
+
problems.push({ filePath, message: `type '${statement.name.text}' is the name the generated module gives the run's result; call this shape something else.` });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const jsDoc = readLeadingJsDoc(statement, sourceFile);
|
|
172
|
+
declarations.push({ name: statement.name.text, text: `${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}` });
|
|
173
|
+
}
|
|
174
|
+
return { declarations, carriedTypeImports, inlinedTypeImports, problems };
|
|
175
|
+
}
|
|
176
|
+
function isRelativeSpecifier(specifier) {
|
|
177
|
+
return specifier.startsWith('./') || specifier.startsWith('../');
|
|
178
|
+
}
|
|
179
|
+
function createJobFolderFiles(options) {
|
|
180
|
+
const collected = [];
|
|
181
|
+
const problems = [];
|
|
182
|
+
const bySpecifier = new Map();
|
|
183
|
+
const folderFiles = {
|
|
184
|
+
collected,
|
|
185
|
+
problems,
|
|
186
|
+
read(specifier) {
|
|
187
|
+
if (bySpecifier.has(specifier))
|
|
188
|
+
return bySpecifier.get(specifier);
|
|
189
|
+
const read = options.readStageFile?.(specifier);
|
|
190
|
+
if (!read) {
|
|
191
|
+
bySpecifier.set(specifier, undefined);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const sourceFile = ts.createSourceFile(read.filePath, read.source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
195
|
+
const entry = { sourceFile, filePath: read.filePath };
|
|
196
|
+
bySpecifier.set(specifier, entry);
|
|
197
|
+
const types = readFileTypes(sourceFile, read.filePath, options, folderFiles);
|
|
198
|
+
problems.push(...types.problems);
|
|
199
|
+
collected.push({ filePath: read.filePath, declarations: types.declarations, inlinedTypeImports: types.inlinedTypeImports, carriedTypeImports: types.carriedTypeImports });
|
|
200
|
+
return entry;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
return folderFiles;
|
|
204
|
+
}
|
|
205
|
+
/** `import { mapFunction } from './map'`: every value imported by name, by the name this file calls it. */
|
|
206
|
+
function readValueImports(sourceFile) {
|
|
207
|
+
const valueImports = new Map();
|
|
208
|
+
for (const statement of sourceFile.statements) {
|
|
209
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
210
|
+
continue;
|
|
211
|
+
const clause = statement.importClause;
|
|
212
|
+
if (!clause || clause.isTypeOnly || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
213
|
+
continue;
|
|
214
|
+
for (const element of clause.namedBindings.elements) {
|
|
215
|
+
if (element.isTypeOnly)
|
|
216
|
+
continue;
|
|
217
|
+
valueImports.set(element.name.text, { specifier: statement.moduleSpecifier.text, importedName: (element.propertyName ?? element.name).text });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return valueImports;
|
|
221
|
+
}
|
|
222
|
+
/** The exported function of that name in a stage file: `export const mapFunction = …` or `export function mapFunction…`. */
|
|
223
|
+
function findExportedFunction(sourceFile, name) {
|
|
224
|
+
for (const statement of sourceFile.statements) {
|
|
225
|
+
if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement) && statement.name?.text === name)
|
|
226
|
+
return statement;
|
|
227
|
+
if (!ts.isVariableStatement(statement) || !hasExportModifier(statement))
|
|
228
|
+
continue;
|
|
229
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
230
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name)
|
|
231
|
+
continue;
|
|
232
|
+
const initializer = declaration.initializer;
|
|
233
|
+
if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)))
|
|
234
|
+
return initializer;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
function readStages(literal, filePath, sourceFile, folderFiles) {
|
|
123
240
|
const problems = [];
|
|
124
241
|
const stages = [];
|
|
242
|
+
const valueImports = readValueImports(sourceFile);
|
|
125
243
|
let inputType;
|
|
126
244
|
let resultType;
|
|
245
|
+
/** The function a stage names, in the file it is imported from: `getInputData: getInputDataFunction`. */
|
|
246
|
+
function findReferencedStage(stageName, referenced) {
|
|
247
|
+
const imported = valueImports.get(referenced);
|
|
248
|
+
if (!imported) {
|
|
249
|
+
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.` });
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
if (!imported.specifier.startsWith('./') && !imported.specifier.startsWith('../')) {
|
|
253
|
+
problems.push({ filePath, message: `stage '${stageName}' comes from '${imported.specifier}'; a stage is imported from a file beside the job, such as './${stageName}'.` });
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const stageFile = folderFiles.read(imported.specifier);
|
|
257
|
+
if (!stageFile) {
|
|
258
|
+
problems.push({ filePath, message: `stage '${stageName}' is imported from '${imported.specifier}', which is not a file beside the job the generator can read.` });
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const handler = findExportedFunction(stageFile.sourceFile, imported.importedName);
|
|
262
|
+
if (!handler) {
|
|
263
|
+
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.` });
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return { handler, sourceFile: stageFile.sourceFile, filePath: stageFile.filePath };
|
|
267
|
+
}
|
|
127
268
|
for (const property of literal.properties) {
|
|
128
269
|
const stageName = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined;
|
|
129
270
|
if (stageName === undefined || !JOB_STAGE_NAMES.includes(stageName)) {
|
|
130
271
|
problems.push({ filePath, message: `'${stageName ?? 'a stage'}' is not a stage; a job declares ${JOB_STAGE_NAMES.join(', ')}.` });
|
|
131
272
|
continue;
|
|
132
273
|
}
|
|
274
|
+
// Where the annotations are read from: the definition file for an inline stage, the stage's own file otherwise.
|
|
133
275
|
let handler;
|
|
276
|
+
let annotatedIn = { sourceFile, filePath };
|
|
134
277
|
if (ts.isMethodDeclaration(property))
|
|
135
278
|
handler = property;
|
|
136
279
|
else if (ts.isPropertyAssignment(property) && (ts.isArrowFunction(property.initializer) || ts.isFunctionExpression(property.initializer)))
|
|
137
280
|
handler = property.initializer;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
281
|
+
else {
|
|
282
|
+
const referenced = ts.isShorthandPropertyAssignment(property)
|
|
283
|
+
? property.name.text
|
|
284
|
+
: ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)
|
|
285
|
+
? property.initializer.text
|
|
286
|
+
: undefined;
|
|
287
|
+
if (referenced === undefined) {
|
|
288
|
+
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.` });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const found = findReferencedStage(stageName, referenced);
|
|
292
|
+
if (!found)
|
|
293
|
+
continue;
|
|
294
|
+
handler = found.handler;
|
|
295
|
+
annotatedIn = { sourceFile: found.sourceFile, filePath: found.filePath };
|
|
141
296
|
}
|
|
142
297
|
stages.push(stageName);
|
|
143
298
|
if (stageName === 'getInputData') {
|
|
144
299
|
const parameter = handler.parameters[0];
|
|
145
300
|
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." });
|
|
301
|
+
problems.push({ filePath: annotatedIn.filePath, message: "getInputData has no type on its first parameter; a run's input shape is read from it." });
|
|
147
302
|
}
|
|
148
|
-
inputType = parameter?.type?.getText(sourceFile);
|
|
303
|
+
inputType = parameter?.type?.getText(annotatedIn.sourceFile);
|
|
149
304
|
}
|
|
150
305
|
if (stageName === 'summarize') {
|
|
151
306
|
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);
|
|
307
|
+
problems.push({ filePath: annotatedIn.filePath, message: "summarize has no return type annotation; a run's result shape is read from it." });
|
|
308
|
+
resultType = handler.type?.getText(annotatedIn.sourceFile);
|
|
154
309
|
}
|
|
155
310
|
}
|
|
156
311
|
if (!stages.includes('getInputData'))
|
|
@@ -162,46 +317,27 @@ function readStages(literal, filePath, sourceFile) {
|
|
|
162
317
|
}
|
|
163
318
|
export function readJobContract(filePath, source, options) {
|
|
164
319
|
const problems = [];
|
|
165
|
-
const
|
|
320
|
+
const posixPath = toPosixPath(filePath);
|
|
321
|
+
const fileName = nodePath.basename(posixPath);
|
|
166
322
|
const nameMatch = jobFileNamePattern.exec(fileName);
|
|
167
323
|
if (!nameMatch)
|
|
168
|
-
return { problems: [{ filePath, message: 'a job
|
|
324
|
+
return { problems: [{ filePath, message: 'a job is declared in <name>.ts, with <name> in camelCase; nothing else declares a job.' }] };
|
|
169
325
|
const name = nameMatch[1];
|
|
326
|
+
const folderName = nodePath.basename(nodePath.dirname(posixPath));
|
|
327
|
+
if (folderName !== name) {
|
|
328
|
+
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.` });
|
|
329
|
+
}
|
|
170
330
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
171
331
|
const header = readScriptTypeHeader(source);
|
|
172
332
|
if (header !== JOB_SCRIPT_TYPE_HEADER) {
|
|
173
333
|
problems.push({ filePath, message: `the leading JSDoc says '@NScriptType ${header ?? '(none)'}'; a job is a ${JOB_SCRIPT_TYPE_HEADER}.` });
|
|
174
334
|
}
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
335
|
+
const folderFiles = createJobFolderFiles(options);
|
|
336
|
+
const fileTypes = readFileTypes(sourceFile, filePath, options, folderFiles);
|
|
337
|
+
problems.push(...fileTypes.problems);
|
|
338
|
+
const { carriedTypeImports, inlinedTypeImports, declarations: typeDeclarations } = fileTypes;
|
|
178
339
|
let contract;
|
|
179
340
|
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
341
|
if (!ts.isVariableStatement(statement))
|
|
206
342
|
continue;
|
|
207
343
|
const found = findDefineJobCall(statement);
|
|
@@ -225,7 +361,7 @@ export function readJobContract(filePath, source, options) {
|
|
|
225
361
|
}
|
|
226
362
|
const declared = readJobDeclaration(declarationArgument, name, filePath);
|
|
227
363
|
problems.push(...declared.problems);
|
|
228
|
-
const stages = readStages(stagesArgument, filePath, sourceFile);
|
|
364
|
+
const stages = readStages(stagesArgument, filePath, sourceFile, folderFiles);
|
|
229
365
|
problems.push(...stages.problems);
|
|
230
366
|
for (const exported of found.exportedStages) {
|
|
231
367
|
if (!JOB_STAGE_NAMES.includes(exported)) {
|
|
@@ -246,8 +382,18 @@ export function readJobContract(filePath, source, options) {
|
|
|
246
382
|
}
|
|
247
383
|
if (!declared.script)
|
|
248
384
|
continue;
|
|
249
|
-
contract = {
|
|
385
|
+
contract = {
|
|
386
|
+
name,
|
|
387
|
+
filePath,
|
|
388
|
+
script: declared.script,
|
|
389
|
+
inputType: stages.inputType,
|
|
390
|
+
resultType: stages.resultType,
|
|
391
|
+
stages: stages.stages,
|
|
392
|
+
exportedStages: found.exportedStages,
|
|
393
|
+
folderFiles: folderFiles.collected,
|
|
394
|
+
};
|
|
250
395
|
}
|
|
396
|
+
problems.push(...folderFiles.problems);
|
|
251
397
|
if (!contract) {
|
|
252
398
|
if (problems.length === 0) {
|
|
253
399
|
problems.push({ filePath, message: 'must declare its script: `export const { getInputData, map, summarize } = defineJob({ name, scriptId, deployments, runParameter, runs }, { ... })`.' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amerilux/netsuite-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|