nucleus-core-ts 0.9.827 → 0.9.828
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/dist/nucleus.config.d.ts +10 -1
- package/dist/src/Services/Integrations/bindings.js +248 -0
- package/dist/src/Services/Integrations/bindings.test.js +693 -0
- package/dist/src/Services/Integrations/collect.js +223 -0
- package/dist/src/Services/Integrations/collect.test.js +581 -0
- package/dist/src/Services/Integrations/cron.js +219 -0
- package/dist/src/Services/Integrations/cron.test.js +141 -0
- package/dist/src/Services/Integrations/fetcher.js +200 -0
- package/dist/src/Services/Integrations/fetcher.test.js +462 -0
- package/dist/src/Services/Integrations/lock.js +119 -0
- package/dist/src/Services/Integrations/lock.test.js +158 -0
- package/dist/src/Services/Integrations/mapping.js +412 -0
- package/dist/src/Services/Integrations/mapping.test.js +837 -0
- package/dist/src/Services/Integrations/oauth.js +216 -0
- package/dist/src/Services/Integrations/oauth.test.js +445 -0
- package/dist/src/Services/Integrations/pagination.js +262 -0
- package/dist/src/Services/Integrations/pagination.test.js +466 -0
- package/dist/src/Services/Integrations/plan.js +428 -0
- package/dist/src/Services/Integrations/plan.test.js +1216 -0
- package/dist/src/Services/Integrations/revert.js +208 -0
- package/dist/src/Services/Integrations/revert.test.js +605 -0
- package/dist/src/Services/Integrations/runner.js +382 -0
- package/dist/src/Services/Integrations/runner.test.js +653 -0
- package/dist/src/Services/Integrations/scheduler.js +114 -0
- package/dist/src/Services/Integrations/scheduler.test.js +95 -0
- package/dist/src/Services/Integrations/transfer.js +239 -0
- package/dist/src/Services/Integrations/transfer.test.js +195 -0
- package/dist/src/Services/Integrations/types.js +9 -0
- package/dist/src/Services/Integrations/writer.js +471 -0
- package/dist/src/Services/Integrations/writer.test.js +925 -0
- package/package.json +1 -1
package/dist/nucleus.config.d.ts
CHANGED
|
@@ -27,7 +27,16 @@ export declare const config: {
|
|
|
27
27
|
readonly './proxy': "./src/Client/Proxy/index.ts";
|
|
28
28
|
};
|
|
29
29
|
/** Additional directories to transpile (dependencies of transpileOnly entries) */
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Directories the FRONTEND imports at runtime.
|
|
32
|
+
*
|
|
33
|
+
* `fe/` is transpiled file by file, not bundled, so an import it makes has to
|
|
34
|
+
* exist under `dist/` at the same relative path. A panel that reads a cron
|
|
35
|
+
* expression shares that reader with the engine — one implementation, two
|
|
36
|
+
* callers — and without the folder here the published bundle asks for a file
|
|
37
|
+
* that was never shipped, which fails the CONSUMER's build, not ours.
|
|
38
|
+
*/
|
|
39
|
+
readonly transpileDirs: readonly ["./src/Client/PubSub", "./src/Services/Integrations"];
|
|
31
40
|
/** Build Configuration */
|
|
32
41
|
readonly build: {
|
|
33
42
|
readonly outDir: "dist";
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoints that need values from somewhere else.
|
|
3
|
+
*
|
|
4
|
+
* Plenty of APIs expose a list and a detail, and the detail is only reachable per
|
|
5
|
+
* item: `/company` gives you the companies, `/company-organization/{companyName}`
|
|
6
|
+
* gives you the people in one of them, and there is no endpoint that gives you
|
|
7
|
+
* all the people. Importing that shape means calling the second endpoint once per
|
|
8
|
+
* record of the first.
|
|
9
|
+
*
|
|
10
|
+
* A binding says where one parameter's value comes from — a fixed value, another
|
|
11
|
+
* endpoint's records, or a column of one of OUR tables. This module works out the
|
|
12
|
+
* order those calls have to happen in, refuses arrangements that cannot terminate,
|
|
13
|
+
* and expands the bindings into the concrete parameter sets to call with.
|
|
14
|
+
*
|
|
15
|
+
* Pure throughout: nothing here fetches or queries. The caller supplies whatever
|
|
16
|
+
* the parents produced.
|
|
17
|
+
*/ import { getByPath } from './mapping';
|
|
18
|
+
/**
|
|
19
|
+
* Ceiling on the calls one endpoint may fan out into.
|
|
20
|
+
*
|
|
21
|
+
* Two bindings over 500 parents each is 250.000 requests at somebody else's API —
|
|
22
|
+
* an accident, not an intent. Hitting this stops the run with an explanation
|
|
23
|
+
* rather than starting a stampede.
|
|
24
|
+
*/ export const MAX_FANOUT_COMBINATIONS = 5000;
|
|
25
|
+
// ── path tokens ─────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Parameter names a path needs, in either convention: `{brace}` (OpenAPI) or
|
|
28
|
+
* `:colon` (nucleus and Express). Duplicates collapse — the same token twice is
|
|
29
|
+
* still one value.
|
|
30
|
+
*/ export function pathTokens(path) {
|
|
31
|
+
const found = new Set();
|
|
32
|
+
const re = /\{([a-zA-Z_][\w-]*)\}|:([a-zA-Z_]\w*)/g;
|
|
33
|
+
let m = re.exec(path);
|
|
34
|
+
while(m !== null){
|
|
35
|
+
const name = m[1] ?? m[2];
|
|
36
|
+
if (name) found.add(name);
|
|
37
|
+
m = re.exec(path);
|
|
38
|
+
}
|
|
39
|
+
return [
|
|
40
|
+
...found
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Substitutes token values into a path.
|
|
45
|
+
*
|
|
46
|
+
* Reports which tokens were left unfilled instead of sending the literal
|
|
47
|
+
* `{employeeNo}` to the far side — that produced a bare HTTP 400 with no clue
|
|
48
|
+
* which parameter was missing.
|
|
49
|
+
*
|
|
50
|
+
* `used` names the value keys consumed as path tokens so the caller does not also
|
|
51
|
+
* append them to the query string.
|
|
52
|
+
*/ export function resolvePath(rawPath, values) {
|
|
53
|
+
const missing = new Set();
|
|
54
|
+
const used = new Set();
|
|
55
|
+
const path = rawPath.replace(/\{([a-zA-Z_][\w-]*)\}|:([a-zA-Z_]\w*)/g, (_full, brace, colon)=>{
|
|
56
|
+
const key = brace ?? colon;
|
|
57
|
+
const v = values[key];
|
|
58
|
+
if (v == null || v === '') {
|
|
59
|
+
missing.add(key);
|
|
60
|
+
return brace ? `{${key}}` : `:${key}`;
|
|
61
|
+
}
|
|
62
|
+
used.add(key);
|
|
63
|
+
return encodeURIComponent(String(v));
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
path,
|
|
67
|
+
missing: [
|
|
68
|
+
...missing
|
|
69
|
+
],
|
|
70
|
+
used
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Orders the endpoints so every parent is fetched before its children.
|
|
75
|
+
*
|
|
76
|
+
* A cycle is refused rather than broken arbitrarily: A needing B needing A has no
|
|
77
|
+
* correct starting point, and picking one would produce an import whose contents
|
|
78
|
+
* depend on which endpoint happened to be first in the list.
|
|
79
|
+
*
|
|
80
|
+
* The returned chain names the cycle in the order it was walked, so an operator
|
|
81
|
+
* can see which binding to remove.
|
|
82
|
+
*/ export function planFetchOrder(endpoints, roots) {
|
|
83
|
+
const byId = new Map(endpoints.map((e)=>[
|
|
84
|
+
e.id,
|
|
85
|
+
e
|
|
86
|
+
]));
|
|
87
|
+
const order = [];
|
|
88
|
+
const settled = new Set();
|
|
89
|
+
const onPath = [];
|
|
90
|
+
const onPathSet = new Set();
|
|
91
|
+
/** Returns the failure that stopped the walk, or null when the branch is fine. */ const visit = (id, requiredBy)=>{
|
|
92
|
+
if (settled.has(id)) return null;
|
|
93
|
+
if (onPathSet.has(id)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: 'cycle',
|
|
97
|
+
chain: [
|
|
98
|
+
...onPath.slice(onPath.indexOf(id)),
|
|
99
|
+
id
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const node = byId.get(id);
|
|
104
|
+
if (!node) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: 'unknown_endpoint',
|
|
108
|
+
missing: id,
|
|
109
|
+
requiredBy: requiredBy ?? id
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
onPath.push(id);
|
|
113
|
+
onPathSet.add(id);
|
|
114
|
+
for (const binding of node.paramBindings ?? []){
|
|
115
|
+
if (binding.from !== 'endpoint') continue;
|
|
116
|
+
const failure = visit(binding.endpointId, id);
|
|
117
|
+
if (failure) return failure;
|
|
118
|
+
}
|
|
119
|
+
onPath.pop();
|
|
120
|
+
onPathSet.delete(id);
|
|
121
|
+
settled.add(id);
|
|
122
|
+
order.push(id);
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
for (const id of roots ?? endpoints.map((e)=>e.id)){
|
|
126
|
+
const failure = visit(id, null);
|
|
127
|
+
if (failure) return failure;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
order
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Turns bindings into the concrete parameter sets to call an endpoint with.
|
|
136
|
+
*
|
|
137
|
+
* One `endpoint` binding over 40 parents means 40 calls. Two independent bindings
|
|
138
|
+
* multiply, which is why {@link MAX_FANOUT_COMBINATIONS} exists.
|
|
139
|
+
*
|
|
140
|
+
* An endpoint with no bindings yields exactly one call with no parameters — the
|
|
141
|
+
* ordinary case, expressed as the degenerate fan-out rather than a special path.
|
|
142
|
+
*
|
|
143
|
+
* A binding whose source produced NOTHING yields no calls at all. That is not an
|
|
144
|
+
* error: a company with no employees is a real answer, and inventing a call with
|
|
145
|
+
* an empty parameter would ask the far side something meaningless.
|
|
146
|
+
*/ export function expandBindings(bindings, sources, limit = MAX_FANOUT_COMBINATIONS) {
|
|
147
|
+
if (!bindings || bindings.length === 0) return {
|
|
148
|
+
ok: true,
|
|
149
|
+
calls: [
|
|
150
|
+
{
|
|
151
|
+
values: {}
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
/** Candidate values for each binding, paired with the parent they came from. */ const axes = [];
|
|
156
|
+
for (const binding of bindings){
|
|
157
|
+
if (binding.from === 'static') {
|
|
158
|
+
axes.push({
|
|
159
|
+
param: binding.param,
|
|
160
|
+
options: [
|
|
161
|
+
{
|
|
162
|
+
value: binding.value
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (binding.from === 'endpoint') {
|
|
169
|
+
const records = sources.parents?.[binding.endpointId] ?? [];
|
|
170
|
+
const options = records.map((record)=>({
|
|
171
|
+
value: getByPath(record, binding.field),
|
|
172
|
+
parent: record
|
|
173
|
+
})).filter((o)=>o.value != null && o.value !== '');
|
|
174
|
+
axes.push({
|
|
175
|
+
param: binding.param,
|
|
176
|
+
options
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const values = sources.entityValues?.[`${binding.entity}.${binding.field}`] ?? [];
|
|
181
|
+
axes.push({
|
|
182
|
+
param: binding.param,
|
|
183
|
+
options: values.filter((v)=>v != null && v !== '').map((value)=>({
|
|
184
|
+
value
|
|
185
|
+
}))
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Any axis with no options means there is nothing to call for.
|
|
189
|
+
if (axes.some((a)=>a.options.length === 0)) return {
|
|
190
|
+
ok: true,
|
|
191
|
+
calls: []
|
|
192
|
+
};
|
|
193
|
+
const combinations = axes.reduce((n, a)=>n * a.options.length, 1);
|
|
194
|
+
if (combinations > limit) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reason: 'too_many',
|
|
198
|
+
combinations,
|
|
199
|
+
limit
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
let calls = [
|
|
203
|
+
{
|
|
204
|
+
values: {}
|
|
205
|
+
}
|
|
206
|
+
];
|
|
207
|
+
for (const axis of axes){
|
|
208
|
+
const next = [];
|
|
209
|
+
for (const call of calls){
|
|
210
|
+
for (const option of axis.options){
|
|
211
|
+
next.push({
|
|
212
|
+
values: {
|
|
213
|
+
...call.values,
|
|
214
|
+
[axis.param]: option.value
|
|
215
|
+
},
|
|
216
|
+
// The LAST parent wins when several axes carry one; a mapping reads
|
|
217
|
+
// `__parent`, singular, and the fan-out axis is the meaningful one.
|
|
218
|
+
parent: option.parent ?? call.parent
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
calls = next;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
calls
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Endpoint ids this endpoint needs fetched first. Used to decide which parents to
|
|
231
|
+
* keep in memory while a run walks the plan.
|
|
232
|
+
*/ export function parentEndpointIds(bindings) {
|
|
233
|
+
const ids = new Set();
|
|
234
|
+
for (const b of bindings ?? [])if (b.from === 'endpoint') ids.add(b.endpointId);
|
|
235
|
+
return [
|
|
236
|
+
...ids
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `entity.field` pairs this endpoint reads from our own tables, so the caller can
|
|
241
|
+
* fetch each column once rather than per binding.
|
|
242
|
+
*/ export function entityValueKeys(bindings) {
|
|
243
|
+
const keys = new Set();
|
|
244
|
+
for (const b of bindings ?? [])if (b.from === 'entity') keys.add(`${b.entity}.${b.field}`);
|
|
245
|
+
return [
|
|
246
|
+
...keys
|
|
247
|
+
];
|
|
248
|
+
}
|