@amerilux/netsuite-api 0.5.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 +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 +37 -7
- package/dist-tooling/jobReader.js +175 -49
- 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 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
|
+
// A stage file's own shapes, then whatever it takes from a service: both are the run's, written where its stages are.
|
|
227
|
+
for (const stageFile of contract.stageFiles) {
|
|
228
|
+
addDeclarations(inlinedTypes, stageFile.filePath, stageFile.declarations);
|
|
229
|
+
for (const typeImport of stageFile.inlinedTypeImports)
|
|
230
|
+
addInlinedTypes(inlinedTypes, typeImport, stageFile.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,27 @@ export interface DeclaredJob {
|
|
|
27
31
|
runParameter: string;
|
|
28
32
|
parameters: JobParameterContract[];
|
|
29
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A file a stage was read from: what it declares, and the type imports it carries. Its declarations are
|
|
36
|
+
* copied into the browser's module like a service's, because a run's shapes are written where its stages are.
|
|
37
|
+
*/
|
|
38
|
+
export interface JobStageFile {
|
|
39
|
+
filePath: string;
|
|
40
|
+
declarations: TypeDeclaration[];
|
|
41
|
+
inlinedTypeImports: InlinedTypeImport[];
|
|
42
|
+
carriedTypeImports: CarriedTypeImport[];
|
|
43
|
+
}
|
|
44
|
+
/** What a job's reader needs beyond a controller's: the files its stages are imported from. */
|
|
45
|
+
export interface ReadJobOptions extends ReadControllerOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Reads a file a stage is imported from, the specifier resolved against the job's own folder, or
|
|
48
|
+
* undefined when there is no such file. Without it, only inline stages can be read.
|
|
49
|
+
*/
|
|
50
|
+
readStageFile?(specifier: string): {
|
|
51
|
+
filePath: string;
|
|
52
|
+
source: string;
|
|
53
|
+
} | undefined;
|
|
54
|
+
}
|
|
30
55
|
export interface JobContract {
|
|
31
56
|
/** The job's name: `closeStaleOrders` for closeStaleOrders.ts, matching `name` in its declaration. */
|
|
32
57
|
name: string;
|
|
@@ -43,11 +68,16 @@ export interface JobContract {
|
|
|
43
68
|
stages: JobStageName[];
|
|
44
69
|
/** The stages the file exports as NetSuite entry points. */
|
|
45
70
|
exportedStages: string[];
|
|
71
|
+
/** The files the stages were imported from, in the order they were read. */
|
|
72
|
+
stageFiles: JobStageFile[];
|
|
46
73
|
}
|
|
47
74
|
export interface JobReadResult {
|
|
48
75
|
contract?: JobContract;
|
|
49
76
|
problems: ControllerProblem[];
|
|
50
77
|
}
|
|
51
|
-
|
|
52
|
-
export declare function
|
|
78
|
+
/** The name a job folder declares: `approveOrders` for api/src/jobs/approveOrders, or undefined when it is not a name. */
|
|
79
|
+
export declare function readJobFolderName(folderName: string): string | undefined;
|
|
80
|
+
/** 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. */
|
|
81
|
+
export declare function jobDefinitionFileName(jobName: string): string;
|
|
82
|
+
export declare function readJobContract(filePath: string, source: string, options: ReadJobOptions): JobReadResult;
|
|
53
83
|
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,165 @@ 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) {
|
|
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) {
|
|
123
204
|
const problems = [];
|
|
124
205
|
const stages = [];
|
|
206
|
+
const stageFiles = [];
|
|
207
|
+
const valueImports = readValueImports(sourceFile);
|
|
208
|
+
const filesBySpecifier = new Map();
|
|
125
209
|
let inputType;
|
|
126
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
|
+
}
|
|
127
250
|
for (const property of literal.properties) {
|
|
128
251
|
const stageName = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined;
|
|
129
252
|
if (stageName === undefined || !JOB_STAGE_NAMES.includes(stageName)) {
|
|
130
253
|
problems.push({ filePath, message: `'${stageName ?? 'a stage'}' is not a stage; a job declares ${JOB_STAGE_NAMES.join(', ')}.` });
|
|
131
254
|
continue;
|
|
132
255
|
}
|
|
256
|
+
// Where the annotations are read from: the definition file for an inline stage, the stage's own file otherwise.
|
|
133
257
|
let handler;
|
|
258
|
+
let annotatedIn = { sourceFile, filePath };
|
|
134
259
|
if (ts.isMethodDeclaration(property))
|
|
135
260
|
handler = property;
|
|
136
261
|
else if (ts.isPropertyAssignment(property) && (ts.isArrowFunction(property.initializer) || ts.isFunctionExpression(property.initializer)))
|
|
137
262
|
handler = property.initializer;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 };
|
|
141
278
|
}
|
|
142
279
|
stages.push(stageName);
|
|
143
280
|
if (stageName === 'getInputData') {
|
|
144
281
|
const parameter = handler.parameters[0];
|
|
145
282
|
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." });
|
|
283
|
+
problems.push({ filePath: annotatedIn.filePath, message: "getInputData has no type on its first parameter; a run's input shape is read from it." });
|
|
147
284
|
}
|
|
148
|
-
inputType = parameter?.type?.getText(sourceFile);
|
|
285
|
+
inputType = parameter?.type?.getText(annotatedIn.sourceFile);
|
|
149
286
|
}
|
|
150
287
|
if (stageName === 'summarize') {
|
|
151
288
|
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);
|
|
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);
|
|
154
291
|
}
|
|
155
292
|
}
|
|
156
293
|
if (!stages.includes('getInputData'))
|
|
@@ -158,50 +295,30 @@ function readStages(literal, filePath, sourceFile) {
|
|
|
158
295
|
if (!stages.includes('map') && !stages.includes('reduce')) {
|
|
159
296
|
problems.push({ filePath, message: 'a job declares a map stage, a reduce stage, or both; NetSuite has nothing to run otherwise.' });
|
|
160
297
|
}
|
|
161
|
-
return { stages, inputType, resultType, problems };
|
|
298
|
+
return { stages, inputType, resultType, stageFiles, problems };
|
|
162
299
|
}
|
|
163
300
|
export function readJobContract(filePath, source, options) {
|
|
164
301
|
const problems = [];
|
|
165
|
-
const
|
|
302
|
+
const posixPath = toPosixPath(filePath);
|
|
303
|
+
const fileName = nodePath.basename(posixPath);
|
|
166
304
|
const nameMatch = jobFileNamePattern.exec(fileName);
|
|
167
305
|
if (!nameMatch)
|
|
168
|
-
return { problems: [{ filePath, message: 'a job
|
|
306
|
+
return { problems: [{ filePath, message: 'a job is declared in <name>.ts, with <name> in camelCase; nothing else declares a job.' }] };
|
|
169
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
|
+
}
|
|
170
312
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
171
313
|
const header = readScriptTypeHeader(source);
|
|
172
314
|
if (header !== JOB_SCRIPT_TYPE_HEADER) {
|
|
173
315
|
problems.push({ filePath, message: `the leading JSDoc says '@NScriptType ${header ?? '(none)'}'; a job is a ${JOB_SCRIPT_TYPE_HEADER}.` });
|
|
174
316
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const typeDeclarations =
|
|
317
|
+
const fileTypes = readFileTypes(sourceFile, filePath, options);
|
|
318
|
+
problems.push(...fileTypes.problems);
|
|
319
|
+
const { carriedTypeImports, inlinedTypeImports, declarations: typeDeclarations } = fileTypes;
|
|
178
320
|
let contract;
|
|
179
321
|
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
322
|
if (!ts.isVariableStatement(statement))
|
|
206
323
|
continue;
|
|
207
324
|
const found = findDefineJobCall(statement);
|
|
@@ -225,7 +342,7 @@ export function readJobContract(filePath, source, options) {
|
|
|
225
342
|
}
|
|
226
343
|
const declared = readJobDeclaration(declarationArgument, name, filePath);
|
|
227
344
|
problems.push(...declared.problems);
|
|
228
|
-
const stages = readStages(stagesArgument, filePath, sourceFile);
|
|
345
|
+
const stages = readStages(stagesArgument, filePath, sourceFile, options);
|
|
229
346
|
problems.push(...stages.problems);
|
|
230
347
|
for (const exported of found.exportedStages) {
|
|
231
348
|
if (!JOB_STAGE_NAMES.includes(exported)) {
|
|
@@ -246,7 +363,16 @@ export function readJobContract(filePath, source, options) {
|
|
|
246
363
|
}
|
|
247
364
|
if (!declared.script)
|
|
248
365
|
continue;
|
|
249
|
-
contract = {
|
|
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
|
+
};
|
|
250
376
|
}
|
|
251
377
|
if (!contract) {
|
|
252
378
|
if (problems.length === 0) {
|
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",
|