@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.
- package/README.md +89 -11
- package/dist/client/apiClient.js +2 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.ts +143 -1
- package/dist/server/apiError.d.ts +2 -0
- package/dist/server/apiError.js +4 -0
- package/dist/server/defineJob.d.ts +95 -0
- package/dist/server/defineJob.js +150 -0
- package/dist/server/endpoint.js +1 -1
- package/dist/server/index.d.ts +8 -3
- package/dist/server/index.js +5 -2
- package/dist/server/jobRuns.d.ts +65 -0
- package/dist/server/jobRuns.js +292 -0
- package/dist/server/suiteletClient.js +2 -1
- package/dist/testing/N/task.d.ts +14 -0
- package/dist/testing/N/task.js +4 -1
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/jobs.d.ts +50 -0
- package/dist/testing/jobs.js +90 -0
- package/dist-tooling/cli/main.js +6 -3
- package/dist-tooling/config.d.ts +45 -4
- package/dist-tooling/config.js +117 -3
- package/dist-tooling/controllerReader.d.ts +24 -2
- package/dist-tooling/controllerReader.js +7 -6
- package/dist-tooling/emit.d.ts +44 -2
- package/dist-tooling/emit.js +132 -7
- package/dist-tooling/generate.d.ts +10 -1
- package/dist-tooling/generate.js +166 -28
- package/dist-tooling/index.d.ts +7 -4
- package/dist-tooling/index.js +3 -1
- package/dist-tooling/jobReader.d.ts +53 -0
- package/dist-tooling/jobReader.js +260 -0
- package/dist-tooling/typesFileReader.d.ts +28 -9
- package/dist-tooling/typesFileReader.js +73 -41
- package/dist-tooling/wireTypes.d.ts +16 -0
- package/dist-tooling/wireTypes.js +50 -0
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ControllerProblem, TypeDeclaration, TypeImportName } from './controllerReader.js';
|
|
2
2
|
/**
|
|
3
|
-
* Reads a
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
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
|
|
64
|
-
const
|
|
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 =
|
|
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 =
|
|
79
|
+
const importedFrom = current.importedNames.get(name);
|
|
76
80
|
if (importedFrom !== undefined) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 ${
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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 &&
|
|
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.
|
|
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",
|