@amerilux/netsuite-api 0.2.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +89 -11
  2. package/dist/client/apiClient.js +2 -2
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/index.d.ts +143 -1
  5. package/dist/server/apiError.d.ts +2 -0
  6. package/dist/server/apiError.js +4 -0
  7. package/dist/server/defineJob.d.ts +95 -0
  8. package/dist/server/defineJob.js +150 -0
  9. package/dist/server/endpoint.js +1 -1
  10. package/dist/server/index.d.ts +8 -3
  11. package/dist/server/index.js +5 -2
  12. package/dist/server/jobRuns.d.ts +65 -0
  13. package/dist/server/jobRuns.js +292 -0
  14. package/dist/server/suiteletClient.js +2 -1
  15. package/dist/testing/N/task.d.ts +14 -0
  16. package/dist/testing/N/task.js +4 -1
  17. package/dist/testing/index.d.ts +3 -0
  18. package/dist/testing/index.js +2 -0
  19. package/dist/testing/jobs.d.ts +50 -0
  20. package/dist/testing/jobs.js +90 -0
  21. package/dist-tooling/cli/main.js +6 -3
  22. package/dist-tooling/config.d.ts +45 -4
  23. package/dist-tooling/config.js +117 -3
  24. package/dist-tooling/controllerReader.d.ts +24 -2
  25. package/dist-tooling/controllerReader.js +7 -6
  26. package/dist-tooling/emit.d.ts +44 -2
  27. package/dist-tooling/emit.js +132 -7
  28. package/dist-tooling/generate.d.ts +10 -1
  29. package/dist-tooling/generate.js +166 -28
  30. package/dist-tooling/index.d.ts +7 -4
  31. package/dist-tooling/index.js +3 -1
  32. package/dist-tooling/jobReader.d.ts +53 -0
  33. package/dist-tooling/jobReader.js +260 -0
  34. package/dist-tooling/typesFileReader.d.ts +28 -9
  35. package/dist-tooling/typesFileReader.js +73 -41
  36. package/dist-tooling/wireTypes.d.ts +16 -0
  37. package/dist-tooling/wireTypes.js +50 -0
  38. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import type { ControllerProblem, TypeDeclaration, TypeImportName } from './controllerReader.js';
2
2
  /**
3
- * Reads a type-only file whose declarations the generator copies into a controller's generated
4
- * module: the generated entity types. A controller names the types it imports from the file; each
5
- * of those is copied with every type it refers to in the same file, so the generated module stands
6
- * on its own. A type built on something the file imports (the repository package's EntityCreate and
7
- * EntityPatch) cannot be copied, because the client would need that package to resolve it.
3
+ * Reads a file whose type declarations the generator copies into a controller's generated module:
4
+ * the generated entity types, or a service (its functions are skipped; its types are what a
5
+ * controller builds wire shapes from). A controller names the types it imports from the file; each
6
+ * of those is copied with every type it refers to, in the same file or, through an import, in
7
+ * another inlinable file, so the generated module stands on its own. A type built on something no
8
+ * inlinable file declares (the repository package's EntityCreate and EntityPatch) cannot be copied,
9
+ * because the client would need that package to resolve it.
8
10
  */
9
11
  export interface InlinableTypeDeclaration extends TypeDeclaration {
10
12
  /** The names of the types the declaration refers to, each once, in the order met. */
@@ -17,15 +19,32 @@ export interface InlinableTypesFile {
17
19
  declarations: Map<string, InlinableTypeDeclaration>;
18
20
  /** Every name the file imports, mapped to the module it comes from. */
19
21
  importedNames: Map<string, string>;
22
+ /** The imports written `Name as Local`: the local name mapped to the name the module exports. */
23
+ renamedImports: Map<string, string>;
20
24
  }
21
- export interface SelectedInlinedTypes {
22
- /** The declarations to copy, in file order, followed by an alias for every renamed import. */
25
+ /** The declarations to copy from one file. */
26
+ export interface SelectedInlinedSection {
27
+ filePath: string;
28
+ /** In file order; the section of the file the controller named ends with an alias for every renamed import. */
23
29
  declarations: TypeDeclaration[];
30
+ }
31
+ export interface SelectedInlinedTypes {
32
+ /** One section per file the selection reached, the file the controller named first. */
33
+ sections: SelectedInlinedSection[];
24
34
  problems: ControllerProblem[];
25
35
  }
36
+ /**
37
+ * Resolves a specifier written in an inlinable file to the inlinable file it names: undefined when
38
+ * the client could not carry it, 'missing' when it names an inlinable file that does not exist (the
39
+ * resolver reports that itself).
40
+ */
41
+ export type ResolveInlinableImport = (specifier: string) => InlinableTypesFile | 'missing' | undefined;
26
42
  export declare function readInlinableTypesFile(filePath: string, source: string): InlinableTypesFile;
27
43
  /**
28
44
  * The declarations a controller's imports pull out of the file: the named types and, transitively,
29
- * what they refer to. A name the file does not declare is a problem reported against the controller.
45
+ * what they refer to. A reference to a name the file imports is followed into the inlinable file the
46
+ * import resolves to, so a service's summary type built on an entity type carries the entity type
47
+ * with it. A name the file does not declare, or an import the client could not carry, is a problem
48
+ * reported against the controller.
30
49
  */
31
- export declare function selectInlinedTypes(file: InlinableTypesFile, names: TypeImportName[], controllerPath: string): SelectedInlinedTypes;
50
+ export declare function selectInlinedTypes(file: InlinableTypesFile, names: TypeImportName[], controllerPath: string, resolveImport?: ResolveInlinableImport): SelectedInlinedTypes;
@@ -1,21 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { readLeadingJsDoc } from './controllerReader.js';
3
- function leftmostIdentifier(name) {
4
- return ts.isIdentifier(name) ? name.text : leftmostIdentifier(name.left);
5
- }
6
- function collectTypeReferences(node, references) {
7
- let referenced;
8
- if (ts.isTypeReferenceNode(node))
9
- referenced = leftmostIdentifier(node.typeName);
10
- else if (ts.isTypeQueryNode(node))
11
- referenced = leftmostIdentifier(node.exprName);
12
- else if (ts.isExpressionWithTypeArguments(node) && ts.isIdentifier(node.expression))
13
- referenced = node.expression.text;
14
- if (referenced !== undefined && !references.includes(referenced))
15
- references.push(referenced);
16
- ts.forEachChild(node, (child) => collectTypeReferences(child, references));
17
- }
18
- function readImportedNames(statement, importedNames) {
3
+ import { collectTypeReferences } from './wireTypes.js';
4
+ function readImportedNames(statement, importedNames, renamedImports) {
19
5
  const clause = statement.importClause;
20
6
  if (!clause || !ts.isStringLiteral(statement.moduleSpecifier))
21
7
  return;
@@ -27,17 +13,22 @@ function readImportedNames(statement, importedNames) {
27
13
  return;
28
14
  if (ts.isNamespaceImport(bindings))
29
15
  importedNames.set(bindings.name.text, moduleSpecifier);
30
- else
31
- for (const element of bindings.elements)
16
+ else {
17
+ for (const element of bindings.elements) {
32
18
  importedNames.set(element.name.text, moduleSpecifier);
19
+ if (element.propertyName)
20
+ renamedImports.set(element.name.text, element.propertyName.text);
21
+ }
22
+ }
33
23
  }
34
24
  export function readInlinableTypesFile(filePath, source) {
35
25
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
36
26
  const declarations = new Map();
37
27
  const importedNames = new Map();
28
+ const renamedImports = new Map();
38
29
  for (const statement of sourceFile.statements) {
39
30
  if (ts.isImportDeclaration(statement)) {
40
- readImportedNames(statement, importedNames);
31
+ readImportedNames(statement, importedNames, renamedImports);
41
32
  continue;
42
33
  }
43
34
  if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement) && !ts.isEnumDeclaration(statement))
@@ -52,46 +43,87 @@ export function readInlinableTypesFile(filePath, source) {
52
43
  const jsDoc = readLeadingJsDoc(statement, sourceFile);
53
44
  declarations.set(statement.name.text, { name: statement.name.text, text: `${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}`, references });
54
45
  }
55
- return { filePath, declarations, importedNames };
46
+ return { filePath, declarations, importedNames, renamedImports };
56
47
  }
57
48
  /**
58
49
  * The declarations a controller's imports pull out of the file: the named types and, transitively,
59
- * what they refer to. A name the file does not declare is a problem reported against the controller.
50
+ * what they refer to. A reference to a name the file imports is followed into the inlinable file the
51
+ * import resolves to, so a service's summary type built on an entity type carries the entity type
52
+ * with it. A name the file does not declare, or an import the client could not carry, is a problem
53
+ * reported against the controller.
60
54
  */
61
- export function selectInlinedTypes(file, names, controllerPath) {
55
+ export function selectInlinedTypes(file, names, controllerPath, resolveImport = () => undefined) {
62
56
  const problems = [];
63
- const selected = new Set();
64
- const pending = names.map((imported) => ({ name: imported.name }));
57
+ const reached = new Map([[file.filePath, file]]);
58
+ const selectedByFile = new Map();
59
+ const selectedIn = (current) => {
60
+ let selected = selectedByFile.get(current.filePath);
61
+ if (!selected) {
62
+ selected = new Set();
63
+ selectedByFile.set(current.filePath, selected);
64
+ }
65
+ return selected;
66
+ };
67
+ const pending = names.map((imported) => ({ file, name: imported.name }));
65
68
  while (pending.length > 0) {
66
- const { name, dependent } = pending.shift();
69
+ const { file: current, name, dependent } = pending.shift();
70
+ const selected = selectedIn(current);
67
71
  if (selected.has(name))
68
72
  continue;
69
- const declaration = file.declarations.get(name);
73
+ const declaration = current.declarations.get(name);
70
74
  if (declaration) {
71
75
  selected.add(name);
72
- pending.push(...declaration.references.map((reference) => ({ name: reference, dependent: name })));
76
+ pending.push(...declaration.references.map((reference) => ({ file: current, name: reference, dependent: name })));
73
77
  continue;
74
78
  }
75
- const importedFrom = file.importedNames.get(name);
79
+ const importedFrom = current.importedNames.get(name);
76
80
  if (importedFrom !== undefined) {
77
- problems.push({
78
- filePath: controllerPath,
79
- message: dependent
80
- ? `type '${dependent}' (${file.filePath}) is built on ${name} from '${importedFrom}', which the client cannot carry; write the wire shape out in the controller instead.`
81
- : `type '${name}' is imported into ${file.filePath} from '${importedFrom}', not declared there; the client cannot carry it.`,
82
- });
81
+ const imported = resolveImport(importedFrom);
82
+ const exportedName = current.renamedImports.get(name);
83
+ if (imported === 'missing') {
84
+ // The resolver reported the missing file; the reference has nothing to say beyond that.
85
+ }
86
+ else if (imported && exportedName === undefined) {
87
+ if (!reached.has(imported.filePath))
88
+ reached.set(imported.filePath, imported);
89
+ selected.add(name);
90
+ pending.push({ file: imported, name, dependent });
91
+ }
92
+ else if (imported) {
93
+ problems.push({
94
+ filePath: controllerPath,
95
+ message: dependent
96
+ ? `type '${dependent}' (${current.filePath}) is built on ${name}, imported from '${importedFrom}' as a rename of ${exportedName}; import it under its own name so the client can carry it.`
97
+ : `type '${name}' is imported into ${current.filePath} from '${importedFrom}' as a rename of ${exportedName}; import it under its own name so the client can carry it.`,
98
+ });
99
+ }
100
+ else {
101
+ problems.push({
102
+ filePath: controllerPath,
103
+ message: dependent
104
+ ? `type '${dependent}' (${current.filePath}) is built on ${name} from '${importedFrom}', which the client cannot carry; write the wire shape out in the controller instead.`
105
+ : `type '${name}' is imported into ${current.filePath} from '${importedFrom}', not declared there; the client cannot carry it.`,
106
+ });
107
+ }
83
108
  }
84
109
  else if (dependent === undefined) {
85
- problems.push({ filePath: controllerPath, message: `type '${name}' is not declared in ${file.filePath}.` });
110
+ problems.push({ filePath: controllerPath, message: `type '${name}' is not declared in ${current.filePath}.` });
86
111
  }
87
112
  // Anything else a declaration refers to is a global (Date, Record, Array): nothing to copy.
88
113
  }
89
- const declarations = Array.from(file.declarations.values())
90
- .filter((declaration) => selected.has(declaration.name))
91
- .map(({ name, text }) => ({ name, text }));
114
+ const sections = Array.from(reached.values()).map((current) => {
115
+ const selected = selectedByFile.get(current.filePath);
116
+ return {
117
+ filePath: current.filePath,
118
+ declarations: Array.from(current.declarations.values())
119
+ .filter((declaration) => selected?.has(declaration.name))
120
+ .map(({ name, text }) => ({ name, text })),
121
+ };
122
+ });
123
+ const resolvedNames = new Set(sections.flatMap((section) => section.declarations.map((declaration) => declaration.name)));
92
124
  for (const imported of names) {
93
- if (imported.alias && selected.has(imported.name))
94
- declarations.push({ name: imported.alias, text: `export type ${imported.alias} = ${imported.name};` });
125
+ if (imported.alias && resolvedNames.has(imported.name))
126
+ sections[0].declarations.push({ name: imported.alias, text: `export type ${imported.alias} = ${imported.name};` });
95
127
  }
96
- return { declarations, problems };
128
+ return { sections: sections.filter((section, index) => index === 0 || section.declarations.length > 0), problems };
97
129
  }
@@ -0,0 +1,16 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * JSON carries no dates: a Date on the wire is an ISO 8601 string in both directions, and neither
4
+ * side revives it. So the generated client module says `string` wherever a wire shape says `Date`,
5
+ * and a request shape may not carry a Date at all, because the handler would receive a string where
6
+ * its annotation promises a Date. The shapes are text (the declarations as written, the handler
7
+ * annotations), so both operations parse the text and work on the syntax tree.
8
+ */
9
+ /** A fragment is a whole declaration (`export interface X { ... }`) or a bare type annotation (`Date[]`). */
10
+ export type WireFragmentKind = 'declaration' | 'type';
11
+ /** Collects, in order met and each once, the names a node refers to: type references, heritage clauses and typeof targets. */
12
+ export declare function collectTypeReferences(node: ts.Node, references: string[]): void;
13
+ /** The names a fragment refers to, `Date` included when it does. */
14
+ export declare function readReferencedNames(text: string, kind: WireFragmentKind): string[];
15
+ /** The fragment with every `Date` type reference written as `string`, everything else byte for byte as it was. */
16
+ export declare function writeDatesAsStrings(text: string, kind: WireFragmentKind): string;
@@ -0,0 +1,50 @@
1
+ import ts from 'typescript';
2
+ const TYPE_FRAGMENT_PREFIX = 'type __WireFragment = ';
3
+ function parseFragment(text, kind) {
4
+ const source = kind === 'type' ? `${TYPE_FRAGMENT_PREFIX}${text};` : text;
5
+ return { sourceFile: ts.createSourceFile('wire.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS), offset: kind === 'type' ? TYPE_FRAGMENT_PREFIX.length : 0 };
6
+ }
7
+ function leftmostIdentifier(name) {
8
+ return ts.isIdentifier(name) ? name.text : leftmostIdentifier(name.left);
9
+ }
10
+ /** Collects, in order met and each once, the names a node refers to: type references, heritage clauses and typeof targets. */
11
+ export function collectTypeReferences(node, references) {
12
+ let referenced;
13
+ if (ts.isTypeReferenceNode(node))
14
+ referenced = leftmostIdentifier(node.typeName);
15
+ else if (ts.isTypeQueryNode(node))
16
+ referenced = leftmostIdentifier(node.exprName);
17
+ else if (ts.isExpressionWithTypeArguments(node) && ts.isIdentifier(node.expression))
18
+ referenced = node.expression.text;
19
+ if (referenced !== undefined && !references.includes(referenced))
20
+ references.push(referenced);
21
+ ts.forEachChild(node, (child) => collectTypeReferences(child, references));
22
+ }
23
+ /** The names a fragment refers to, `Date` included when it does. */
24
+ export function readReferencedNames(text, kind) {
25
+ const { sourceFile } = parseFragment(text, kind);
26
+ const references = [];
27
+ collectTypeReferences(sourceFile, references);
28
+ return references.filter((name) => name !== '__WireFragment');
29
+ }
30
+ function isDateReference(node) {
31
+ return ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === 'Date' && node.typeArguments === undefined;
32
+ }
33
+ function collectNodes(node, matches, found) {
34
+ if (matches(node))
35
+ found.push(node);
36
+ ts.forEachChild(node, (child) => collectNodes(child, matches, found));
37
+ }
38
+ /** The fragment with every `Date` type reference written as `string`, everything else byte for byte as it was. */
39
+ export function writeDatesAsStrings(text, kind) {
40
+ const { sourceFile, offset } = parseFragment(text, kind);
41
+ const references = [];
42
+ collectNodes(sourceFile, isDateReference, references);
43
+ let result = text;
44
+ for (const reference of references.sort((left, right) => right.getStart(sourceFile) - left.getStart(sourceFile))) {
45
+ const start = reference.getStart(sourceFile) - offset;
46
+ const end = reference.getEnd() - offset;
47
+ result = `${result.slice(0, start)}string${result.slice(end)}`;
48
+ }
49
+ return result;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerilux/netsuite-api",
3
- "version": "0.2.1",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "The API layer for a NetSuite single-page app: endpoints served by a Restlet or a Suitelet, a typed browser client, SuiteScript module stubs for tests, and a generator that writes the client module from the controllers.",
6
6
  "license": "MIT",