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.
Files changed (31) hide show
  1. package/dist/nucleus.config.d.ts +10 -1
  2. package/dist/src/Services/Integrations/bindings.js +248 -0
  3. package/dist/src/Services/Integrations/bindings.test.js +693 -0
  4. package/dist/src/Services/Integrations/collect.js +223 -0
  5. package/dist/src/Services/Integrations/collect.test.js +581 -0
  6. package/dist/src/Services/Integrations/cron.js +219 -0
  7. package/dist/src/Services/Integrations/cron.test.js +141 -0
  8. package/dist/src/Services/Integrations/fetcher.js +200 -0
  9. package/dist/src/Services/Integrations/fetcher.test.js +462 -0
  10. package/dist/src/Services/Integrations/lock.js +119 -0
  11. package/dist/src/Services/Integrations/lock.test.js +158 -0
  12. package/dist/src/Services/Integrations/mapping.js +412 -0
  13. package/dist/src/Services/Integrations/mapping.test.js +837 -0
  14. package/dist/src/Services/Integrations/oauth.js +216 -0
  15. package/dist/src/Services/Integrations/oauth.test.js +445 -0
  16. package/dist/src/Services/Integrations/pagination.js +262 -0
  17. package/dist/src/Services/Integrations/pagination.test.js +466 -0
  18. package/dist/src/Services/Integrations/plan.js +428 -0
  19. package/dist/src/Services/Integrations/plan.test.js +1216 -0
  20. package/dist/src/Services/Integrations/revert.js +208 -0
  21. package/dist/src/Services/Integrations/revert.test.js +605 -0
  22. package/dist/src/Services/Integrations/runner.js +382 -0
  23. package/dist/src/Services/Integrations/runner.test.js +653 -0
  24. package/dist/src/Services/Integrations/scheduler.js +114 -0
  25. package/dist/src/Services/Integrations/scheduler.test.js +95 -0
  26. package/dist/src/Services/Integrations/transfer.js +239 -0
  27. package/dist/src/Services/Integrations/transfer.test.js +195 -0
  28. package/dist/src/Services/Integrations/types.js +9 -0
  29. package/dist/src/Services/Integrations/writer.js +471 -0
  30. package/dist/src/Services/Integrations/writer.test.js +925 -0
  31. package/package.json +1 -1
@@ -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
- readonly transpileDirs: readonly ["./src/Client/PubSub"];
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
+ }