@amerilux/netsuite-api 0.6.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 CHANGED
@@ -242,7 +242,7 @@ export interface CloseStaleRequest { olderThanDays: number }
242
242
  export const getInputDataFunction = (input: CloseStaleRequest): StaleOrder[] => listStaleOrders(input.olderThanDays);
243
243
  ```
244
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.
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
246
 
247
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.
248
248
 
@@ -223,11 +223,11 @@ export function planClientGeneration({ config, fileSystem }) {
223
223
  problems.push({ filePath: jobLabel, message: `a controller is named '${contract.name}' too; their generated modules would be the same file.` });
224
224
  }
225
225
  const inlinedTypes = [];
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);
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);
231
231
  }
232
232
  for (const typeImport of contract.inlinedTypeImports)
233
233
  addInlinedTypes(inlinedTypes, typeImport, jobLabel);
@@ -32,10 +32,11 @@ export interface DeclaredJob {
32
32
  parameters: JobParameterContract[];
33
33
  }
34
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.
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.
37
38
  */
38
- export interface JobStageFile {
39
+ export interface JobFolderFile {
39
40
  filePath: string;
40
41
  declarations: TypeDeclaration[];
41
42
  inlinedTypeImports: InlinedTypeImport[];
@@ -68,8 +69,8 @@ export interface JobContract {
68
69
  stages: JobStageName[];
69
70
  /** The stages the file exports as NetSuite entry points. */
70
71
  exportedStages: string[];
71
- /** The files the stages were imported from, in the order they were read. */
72
- stageFiles: JobStageFile[];
72
+ /** The files of the job's folder that were read, in that order: its stages, and whatever they name. */
73
+ folderFiles: JobFolderFile[];
73
74
  }
74
75
  export interface JobReadResult {
75
76
  contract?: JobContract;
@@ -133,7 +133,7 @@ function readJobDeclaration(declaration, jobName, filePath) {
133
133
  * The exported type declarations of a file and the type imports it carries, for a job's definition file
134
134
  * or for one of its stage files: both end up in the browser's module, so both are read the same way.
135
135
  */
136
- function readFileTypes(sourceFile, filePath, options) {
136
+ function readFileTypes(sourceFile, filePath, options, folderFiles) {
137
137
  const declarations = [];
138
138
  const carriedTypeImports = [];
139
139
  const inlinedTypeImports = [];
@@ -141,7 +141,6 @@ function readFileTypes(sourceFile, filePath, options) {
141
141
  for (const statement of sourceFile.statements) {
142
142
  if (ts.isImportDeclaration(statement)) {
143
143
  const result = readTypeImport(statement, filePath, options);
144
- problems.push(...result.problems);
145
144
  if (result.read?.kind === 'carried')
146
145
  carriedTypeImports.push(result.read.typeImport);
147
146
  else if (result.read?.kind === 'inlined')
@@ -149,6 +148,14 @@ function readFileTypes(sourceFile, filePath, options) {
149
148
  else if (result.read?.kind === 'controller') {
150
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." });
151
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);
152
159
  continue;
153
160
  }
154
161
  if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement) && !ts.isEnumDeclaration(statement))
@@ -166,6 +173,35 @@ function readFileTypes(sourceFile, filePath, options) {
166
173
  }
167
174
  return { declarations, carriedTypeImports, inlinedTypeImports, problems };
168
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
+ }
169
205
  /** `import { mapFunction } from './map'`: every value imported by name, by the name this file calls it. */
170
206
  function readValueImports(sourceFile) {
171
207
  const valueImports = new Map();
@@ -200,30 +236,12 @@ function findExportedFunction(sourceFile, name) {
200
236
  }
201
237
  return undefined;
202
238
  }
203
- function readStages(literal, filePath, sourceFile, options) {
239
+ function readStages(literal, filePath, sourceFile, folderFiles) {
204
240
  const problems = [];
205
241
  const stages = [];
206
- const stageFiles = [];
207
242
  const valueImports = readValueImports(sourceFile);
208
- const filesBySpecifier = new Map();
209
243
  let inputType;
210
244
  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
245
  /** The function a stage names, in the file it is imported from: `getInputData: getInputDataFunction`. */
228
246
  function findReferencedStage(stageName, referenced) {
229
247
  const imported = valueImports.get(referenced);
@@ -235,7 +253,7 @@ function readStages(literal, filePath, sourceFile, options) {
235
253
  problems.push({ filePath, message: `stage '${stageName}' comes from '${imported.specifier}'; a stage is imported from a file beside the job, such as './${stageName}'.` });
236
254
  return undefined;
237
255
  }
238
- const stageFile = readStageFile(imported.specifier);
256
+ const stageFile = folderFiles.read(imported.specifier);
239
257
  if (!stageFile) {
240
258
  problems.push({ filePath, message: `stage '${stageName}' is imported from '${imported.specifier}', which is not a file beside the job the generator can read.` });
241
259
  return undefined;
@@ -295,7 +313,7 @@ function readStages(literal, filePath, sourceFile, options) {
295
313
  if (!stages.includes('map') && !stages.includes('reduce')) {
296
314
  problems.push({ filePath, message: 'a job declares a map stage, a reduce stage, or both; NetSuite has nothing to run otherwise.' });
297
315
  }
298
- return { stages, inputType, resultType, stageFiles, problems };
316
+ return { stages, inputType, resultType, problems };
299
317
  }
300
318
  export function readJobContract(filePath, source, options) {
301
319
  const problems = [];
@@ -314,7 +332,8 @@ export function readJobContract(filePath, source, options) {
314
332
  if (header !== JOB_SCRIPT_TYPE_HEADER) {
315
333
  problems.push({ filePath, message: `the leading JSDoc says '@NScriptType ${header ?? '(none)'}'; a job is a ${JOB_SCRIPT_TYPE_HEADER}.` });
316
334
  }
317
- const fileTypes = readFileTypes(sourceFile, filePath, options);
335
+ const folderFiles = createJobFolderFiles(options);
336
+ const fileTypes = readFileTypes(sourceFile, filePath, options, folderFiles);
318
337
  problems.push(...fileTypes.problems);
319
338
  const { carriedTypeImports, inlinedTypeImports, declarations: typeDeclarations } = fileTypes;
320
339
  let contract;
@@ -342,7 +361,7 @@ export function readJobContract(filePath, source, options) {
342
361
  }
343
362
  const declared = readJobDeclaration(declarationArgument, name, filePath);
344
363
  problems.push(...declared.problems);
345
- const stages = readStages(stagesArgument, filePath, sourceFile, options);
364
+ const stages = readStages(stagesArgument, filePath, sourceFile, folderFiles);
346
365
  problems.push(...stages.problems);
347
366
  for (const exported of found.exportedStages) {
348
367
  if (!JOB_STAGE_NAMES.includes(exported)) {
@@ -371,9 +390,10 @@ export function readJobContract(filePath, source, options) {
371
390
  resultType: stages.resultType,
372
391
  stages: stages.stages,
373
392
  exportedStages: found.exportedStages,
374
- stageFiles: stages.stageFiles,
393
+ folderFiles: folderFiles.collected,
375
394
  };
376
395
  }
396
+ problems.push(...folderFiles.problems);
377
397
  if (!contract) {
378
398
  if (problems.length === 0) {
379
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.6.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",