@dudousxd/nestjs-catalog 0.13.0 → 0.15.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/dist/catalog.controller.js +66 -6
- package/dist/catalog.csv.d.ts +89 -0
- package/dist/catalog.csv.js +163 -0
- package/dist/catalog.filters.d.ts +174 -0
- package/dist/catalog.filters.js +272 -0
- package/dist/catalog.identifiers.d.ts +161 -0
- package/dist/catalog.identifiers.js +195 -0
- package/dist/catalog.pipeline.d.ts +392 -35
- package/dist/catalog.pipeline.js +175 -25
- package/dist/catalog.query-cache.d.ts +0 -2
- package/dist/catalog.query-cache.js +0 -18
- package/dist/catalog.query.d.ts +43 -0
- package/dist/catalog.query.js +5 -0
- package/dist/catalog.service.d.ts +51 -0
- package/dist/catalog.service.js +126 -3
- package/dist/catalog.store.d.ts +84 -22
- package/dist/catalog.store.js +36 -68
- package/dist/catalog.types.d.ts +64 -0
- package/dist/client.d.ts +70 -2
- package/dist/client.js +66 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +24 -3
- package/dist/stores/mikro-orm-read.store.d.ts +8 -2
- package/dist/stores/mikro-orm-read.store.js +77 -11
- package/package.json +1 -1
package/dist/catalog.pipeline.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* systems each believing they decide when a load runs.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
|
|
12
|
+
exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
|
|
13
13
|
exports.isConnectorKind = isConnectorKind;
|
|
14
14
|
exports.isTransformLanguage = isTransformLanguage;
|
|
15
15
|
exports.isWorkflowNodeKind = isWorkflowNodeKind;
|
|
16
16
|
exports.isWorkflowStatus = isWorkflowStatus;
|
|
17
17
|
exports.isWorkflowExecutionMode = isWorkflowExecutionMode;
|
|
18
|
+
exports.readWorkflowCallOutput = readWorkflowCallOutput;
|
|
18
19
|
exports.validateWorkflow = validateWorkflow;
|
|
19
20
|
exports.workflowRunOrder = workflowRunOrder;
|
|
20
21
|
exports.workflowGraphHash = workflowGraphHash;
|
|
@@ -92,12 +93,13 @@ exports.TRANSFORM_RUNNER = Symbol('TRANSFORM_RUNNER');
|
|
|
92
93
|
/**
|
|
93
94
|
* What a node can be.
|
|
94
95
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
96
|
+
* The first three are exactly the three verbs the existing connector runner
|
|
97
|
+
* already performs in sequence: fetch, transform, publish. The fourth hands a
|
|
98
|
+
* position in the graph to a durable workflow that already exists in the
|
|
99
|
+
* deployment. Nothing here is a kind this service cannot execute, which is the
|
|
100
|
+
* same rule {@link CONNECTOR_KINDS} follows — a kind that exists in the type
|
|
101
|
+
* and throws at run time is worse than one that is absent, because the first
|
|
102
|
+
* looks supported in a palette.
|
|
101
103
|
*
|
|
102
104
|
* The kinds that were considered and rejected, since a small vocabulary is only
|
|
103
105
|
* defensible if the omissions are:
|
|
@@ -115,6 +117,15 @@ exports.TRANSFORM_RUNNER = Symbol('TRANSFORM_RUNNER');
|
|
|
115
117
|
* A `merge` kind would have had to carry a strategy field whose values the
|
|
116
118
|
* runner would have to implement one by one, and an unimplemented strategy in
|
|
117
119
|
* a dropdown is the failure this list exists to avoid.
|
|
120
|
+
* - **call a durable *step*** — the sibling of {@link WorkflowCallNode} that
|
|
121
|
+
* somebody will eventually come looking for, and it cannot be built. A
|
|
122
|
+
* durable step has no global identity: it is dispatched by a routing name
|
|
123
|
+
* that a worker subscribes to, and within a run it is addressed by its `seq`
|
|
124
|
+
* — a position in one workflow's history. There is no "run step X" entry
|
|
125
|
+
* point on the engine to call, no lifecycle of its own to await, and nothing
|
|
126
|
+
* to cancel. A workflow is the smallest thing that is addressable from
|
|
127
|
+
* outside a run, which is why `call` names one and not a step. If a step is
|
|
128
|
+
* what you want, the thing to call is a one-step workflow wrapping it.
|
|
118
129
|
*/
|
|
119
130
|
exports.WORKFLOW_NODE_KINDS = [
|
|
120
131
|
/** Reads records out of a system. The roots of the graph. */
|
|
@@ -123,6 +134,8 @@ exports.WORKFLOW_NODE_KINDS = [
|
|
|
123
134
|
'transform',
|
|
124
135
|
/** Writes into an object type and commits. Exactly one per workflow. */
|
|
125
136
|
'sink',
|
|
137
|
+
/** Hands this position to an existing durable workflow. See {@link WorkflowCallNode}. */
|
|
138
|
+
'call',
|
|
126
139
|
];
|
|
127
140
|
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
128
141
|
function isWorkflowNodeKind(value) {
|
|
@@ -236,6 +249,56 @@ exports.WORKFLOW_EXECUTION_MODES = [
|
|
|
236
249
|
function isWorkflowExecutionMode(value) {
|
|
237
250
|
return exports.WORKFLOW_EXECUTION_MODES.some((mode) => mode === value);
|
|
238
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The number in {@link WorkflowCallEnvelope.contract}.
|
|
254
|
+
*
|
|
255
|
+
* A version on the *shape the catalog sends*, separate from the version of the
|
|
256
|
+
* workflow being called, because the two change for different reasons and a
|
|
257
|
+
* callee written against one has to be able to say which. A callee that reads
|
|
258
|
+
* this and does not recognise it should refuse rather than guess — a failed
|
|
259
|
+
* child is a failed node with a name attached, and a guess is a load nobody can
|
|
260
|
+
* account for.
|
|
261
|
+
*/
|
|
262
|
+
exports.WORKFLOW_CALL_CONTRACT = 1;
|
|
263
|
+
/**
|
|
264
|
+
* Read a child's return value as staged rows — or as nothing, or refuse it.
|
|
265
|
+
*
|
|
266
|
+
* Three answers rather than two, because a call has two legitimate purposes and
|
|
267
|
+
* they must not be confused with a bug:
|
|
268
|
+
*
|
|
269
|
+
* - `undefined` — the child returned nothing this graph can read rows from. A
|
|
270
|
+
* perfectly ordinary outcome for a workflow called for its effect, and the
|
|
271
|
+
* node reports zero rows, out loud, in its logs. It is not silently treated
|
|
272
|
+
* as success-with-data: a full sink that then receives nothing refuses to
|
|
273
|
+
* commit an empty snapshot, which is the loud end of this path.
|
|
274
|
+
* - a {@link WorkflowCallOutput} — the child staged rows for this node.
|
|
275
|
+
* - a **throw** — the child answered with `batches`/`rowCount` that are not
|
|
276
|
+
* usable counts. Half a contract is a bug in the callee, and reading it as
|
|
277
|
+
* "no rows" would turn that bug into a load that quietly came out short.
|
|
278
|
+
*
|
|
279
|
+
* The catalog cannot check any of this before the graph runs; there is no
|
|
280
|
+
* schema for a workflow's output anywhere in the durable contract, and no way
|
|
281
|
+
* to reach one if there were. So the check is here, at the one moment the
|
|
282
|
+
* answer exists, and it names what it saw.
|
|
283
|
+
*/
|
|
284
|
+
function readWorkflowCallOutput(value) {
|
|
285
|
+
if (typeof value !== 'object' || value === null)
|
|
286
|
+
return undefined;
|
|
287
|
+
const batches = Reflect.get(value, 'batches');
|
|
288
|
+
const rowCount = Reflect.get(value, 'rowCount');
|
|
289
|
+
if (batches === undefined && rowCount === undefined)
|
|
290
|
+
return undefined;
|
|
291
|
+
if (!isCount(batches) || !isCount(rowCount)) {
|
|
292
|
+
throw new Error(`It answered with batches=${describeCount(batches)} and rowCount=${describeCount(rowCount)}. A workflow that stages rows for a call node returns both as whole numbers of at least zero; returning one of them, or a value that is not a count, would leave this node to guess how much of the stage to read.`);
|
|
293
|
+
}
|
|
294
|
+
return { batches, rowCount };
|
|
295
|
+
}
|
|
296
|
+
function isCount(value) {
|
|
297
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
298
|
+
}
|
|
299
|
+
function describeCount(value) {
|
|
300
|
+
return value === undefined ? 'nothing' : JSON.stringify(value);
|
|
301
|
+
}
|
|
239
302
|
/** Every way a graph can be refused. Exported so a canvas can key off the code. */
|
|
240
303
|
exports.WORKFLOW_ISSUE_CODES = [
|
|
241
304
|
'empty',
|
|
@@ -253,6 +316,7 @@ exports.WORKFLOW_ISSUE_CODES = [
|
|
|
253
316
|
'unreachable',
|
|
254
317
|
'dead-end',
|
|
255
318
|
'transform-not-named',
|
|
319
|
+
'call-not-named',
|
|
256
320
|
];
|
|
257
321
|
/**
|
|
258
322
|
* Everything that makes a graph unrunnable, in one pure function.
|
|
@@ -291,10 +355,10 @@ function validateWorkflow(graph) {
|
|
|
291
355
|
if (issues.length > 0)
|
|
292
356
|
return issues;
|
|
293
357
|
const { outgoing, incoming } = buildAdjacency(nodes, edges);
|
|
294
|
-
const
|
|
358
|
+
const originators = nodes.filter(originatesRows);
|
|
295
359
|
const sinks = nodes.filter((node) => node.kind === 'sink');
|
|
296
360
|
checkNodeWiring(nodes, incoming, outgoing, issues);
|
|
297
|
-
checkEndpoints(
|
|
361
|
+
checkEndpoints(originators, sinks, issues);
|
|
298
362
|
const looped = findCycle(nodes, incoming, outgoing);
|
|
299
363
|
if (looped) {
|
|
300
364
|
issues.push({
|
|
@@ -306,9 +370,27 @@ function validateWorkflow(graph) {
|
|
|
306
370
|
// only unreachable *because* of the cycle, which points at the wrong boxes.
|
|
307
371
|
return issues;
|
|
308
372
|
}
|
|
309
|
-
checkReachability(nodes,
|
|
373
|
+
checkReachability(nodes, originators, sinks, incoming, outgoing, issues);
|
|
310
374
|
return issues;
|
|
311
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Whether a node can produce rows without anything wired into it.
|
|
378
|
+
*
|
|
379
|
+
* A source obviously can. A **call** node can too, and this is the one rule the
|
|
380
|
+
* `call` kind changes rather than extends: the workflow it hands off to may
|
|
381
|
+
* itself read from a system, so a graph of `call → sink` is a real pipeline and
|
|
382
|
+
* refusing it for having "no source" would be false. What is not weakened is
|
|
383
|
+
* that a graph still needs *something* that originates rows and *something*
|
|
384
|
+
* that commits them — a graph of transforms alone is still refused.
|
|
385
|
+
*
|
|
386
|
+
* Every call node counts, not only the ones with no inbound edge, and that is
|
|
387
|
+
* the conservative direction: it makes this the root set for reachability too,
|
|
388
|
+
* so a mid-graph call node cannot make everything downstream of it look
|
|
389
|
+
* unreachable when its own upstream is fine.
|
|
390
|
+
*/
|
|
391
|
+
function originatesRows(node) {
|
|
392
|
+
return node.kind === 'source' || node.kind === 'call';
|
|
393
|
+
}
|
|
312
394
|
/**
|
|
313
395
|
* Index the nodes by id, reporting the ids that cannot be used as one.
|
|
314
396
|
*
|
|
@@ -403,15 +485,48 @@ function checkNodeWiring(nodes, incoming, outgoing, issues) {
|
|
|
403
485
|
message: `Sink "${node.name}" (${node.id}) has an outbound edge. The sink commits the snapshot, so nothing can run after it.`,
|
|
404
486
|
});
|
|
405
487
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
nodeIds: [node.id],
|
|
410
|
-
message: `Transform node "${node.name}" (${node.id}) names no transform, so there is no code for it to run.`,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
488
|
+
const unconfigured = nodeIsUnconfigured(node);
|
|
489
|
+
if (unconfigured)
|
|
490
|
+
issues.push(unconfigured);
|
|
413
491
|
}
|
|
414
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* A node that names none of the thing it exists to run.
|
|
495
|
+
*
|
|
496
|
+
* The two kinds that point at something outside themselves — a transform at
|
|
497
|
+
* stored code, a call at a registered workflow — and both are reported the same
|
|
498
|
+
* way because they are the same mistake: a box on the canvas with nothing
|
|
499
|
+
* behind it, which looks finished and fails at run time.
|
|
500
|
+
*/
|
|
501
|
+
function nodeIsUnconfigured(node) {
|
|
502
|
+
if (node.kind === 'transform' && node.transformId.length === 0) {
|
|
503
|
+
return {
|
|
504
|
+
code: 'transform-not-named',
|
|
505
|
+
nodeIds: [node.id],
|
|
506
|
+
message: `Transform node "${node.name}" (${node.id}) names no transform, so there is no code for it to run.`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (node.kind === 'call')
|
|
510
|
+
return callIsUnnamed(node);
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* A call that names half of what it needs, or nothing at all.
|
|
515
|
+
*
|
|
516
|
+
* Both halves, checked separately, because the version is the one people leave
|
|
517
|
+
* blank: a call that named only a workflow would run whichever version happens
|
|
518
|
+
* to be registered on the day the load runs, which is the single thing this
|
|
519
|
+
* node is built not to do.
|
|
520
|
+
*/
|
|
521
|
+
function callIsUnnamed(node) {
|
|
522
|
+
if (node.callName.length > 0 && node.callVersion.length > 0)
|
|
523
|
+
return undefined;
|
|
524
|
+
return {
|
|
525
|
+
code: 'call-not-named',
|
|
526
|
+
nodeIds: [node.id],
|
|
527
|
+
message: `Call node "${node.name}" (${node.id}) does not name ${node.callName.length === 0 ? 'a workflow to call' : 'a version of the workflow it calls'}. A call pins a name and a version together — without the version it would run whichever one is registered when the load happens, and somebody else's deploy would change what this graph does.`,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
415
530
|
/**
|
|
416
531
|
* That the graph has both ends, and that no two sinks claim the same type.
|
|
417
532
|
*
|
|
@@ -424,12 +539,12 @@ function checkNodeWiring(nodes, incoming, outgoing, issues) {
|
|
|
424
539
|
* model does not pretend otherwise — but two snapshots of one type in one run
|
|
425
540
|
* leaves nothing to say which of them the readers should get.
|
|
426
541
|
*/
|
|
427
|
-
function checkEndpoints(
|
|
428
|
-
if (
|
|
542
|
+
function checkEndpoints(originators, sinks, issues) {
|
|
543
|
+
if (originators.length === 0) {
|
|
429
544
|
issues.push({
|
|
430
545
|
code: 'no-source',
|
|
431
546
|
nodeIds: [],
|
|
432
|
-
message: 'This workflow has no source node,
|
|
547
|
+
message: 'This workflow has nothing that reads: no source node, and no call node handing off to a workflow that reads. Nothing would ever be fetched and the sink would commit an empty snapshot.',
|
|
433
548
|
});
|
|
434
549
|
}
|
|
435
550
|
if (sinks.length === 0) {
|
|
@@ -512,12 +627,12 @@ function peelTails(leftover, outgoing) {
|
|
|
512
627
|
}
|
|
513
628
|
return leftover;
|
|
514
629
|
}
|
|
515
|
-
/** Nodes that
|
|
516
|
-
function checkReachability(nodes,
|
|
517
|
-
const reachableFromSources = walk(
|
|
630
|
+
/** Nodes that nothing reading reaches, and nodes that reach no sink. */
|
|
631
|
+
function checkReachability(nodes, originators, sinks, incoming, outgoing, issues) {
|
|
632
|
+
const reachableFromSources = walk(originators.map((node) => node.id), outgoing);
|
|
518
633
|
const reachesASink = walk(sinks.map((sink) => sink.id), incoming);
|
|
519
634
|
for (const node of nodes) {
|
|
520
|
-
if (
|
|
635
|
+
if (originators.length > 0 && !reachableFromSources.has(node.id)) {
|
|
521
636
|
issues.push({
|
|
522
637
|
code: 'unreachable',
|
|
523
638
|
nodeIds: [node.id],
|
|
@@ -648,6 +763,22 @@ function canonicalNode(node) {
|
|
|
648
763
|
// changed when it did not.
|
|
649
764
|
return JSON.stringify([node.id, node.kind, node.transformId]);
|
|
650
765
|
}
|
|
766
|
+
if (node.kind === 'call') {
|
|
767
|
+
// The called version IS in here, and that is the opposite choice from a
|
|
768
|
+
// transform above — for the reason the two differ. A transform's version is
|
|
769
|
+
// this catalog's own record of an edit somebody made here; a call's version
|
|
770
|
+
// is a different piece of code entirely. Repointing a node from `foo@1` to
|
|
771
|
+
// `foo@2` changes what the load does as surely as rewiring it does, so it
|
|
772
|
+
// is a new version of the graph and the run that used the old one stays
|
|
773
|
+
// identifiable.
|
|
774
|
+
return JSON.stringify([
|
|
775
|
+
node.id,
|
|
776
|
+
node.kind,
|
|
777
|
+
node.callName,
|
|
778
|
+
node.callVersion,
|
|
779
|
+
sortedEntries(node.config),
|
|
780
|
+
]);
|
|
781
|
+
}
|
|
651
782
|
return JSON.stringify([node.id, node.kind, node.targetType, node.mode ?? 'full']);
|
|
652
783
|
}
|
|
653
784
|
function sortedEntries(config) {
|
|
@@ -692,6 +823,18 @@ function isWorkflowNode(value) {
|
|
|
692
823
|
if (kind === 'sink') {
|
|
693
824
|
return typeof Reflect.get(value, 'targetType') === 'string';
|
|
694
825
|
}
|
|
826
|
+
if (kind === 'call') {
|
|
827
|
+
// Both strings, and the config object, exactly as strictly as a source's:
|
|
828
|
+
// a stored call node missing its version is a node that would run whatever
|
|
829
|
+
// is registered today, which is the failure the pin exists to remove — and
|
|
830
|
+
// a graph that half-narrows is a load that runs nine nodes of ten.
|
|
831
|
+
const config = Reflect.get(value, 'config');
|
|
832
|
+
return (typeof Reflect.get(value, 'callName') === 'string' &&
|
|
833
|
+
typeof Reflect.get(value, 'callVersion') === 'string' &&
|
|
834
|
+
typeof config === 'object' &&
|
|
835
|
+
config !== null &&
|
|
836
|
+
!Array.isArray(config));
|
|
837
|
+
}
|
|
695
838
|
const sourceKind = Reflect.get(value, 'sourceKind');
|
|
696
839
|
const config = Reflect.get(value, 'config');
|
|
697
840
|
return isConnectorKind(sourceKind) && typeof config === 'object' && config !== null;
|
|
@@ -716,7 +859,14 @@ function supportsWorkflows(store) {
|
|
|
716
859
|
// save and not the transition would narrow cleanly here and then fail one
|
|
717
860
|
// call later, in the middle of an apply that has already written types and
|
|
718
861
|
// transforms into the target.
|
|
719
|
-
typeof store.publishWorkflow === 'function'
|
|
862
|
+
typeof store.publishWorkflow === 'function' &&
|
|
863
|
+
// Asked for by name for the same reason `publishWorkflow` is, and it earned
|
|
864
|
+
// the place the hard way: a schedule authored on a graph is worthless if the
|
|
865
|
+
// store cannot hold one, and a predicate that narrowed without checking
|
|
866
|
+
// would let the schedule route resolve, accept a cron, and throw
|
|
867
|
+
// "saveWorkflowSchedule is not a function" at the person who typed it. This
|
|
868
|
+
// is the surface a scheduling incident already came through once.
|
|
869
|
+
typeof store.saveWorkflowSchedule === 'function');
|
|
720
870
|
}
|
|
721
871
|
/**
|
|
722
872
|
* Whether this store keeps a transform's history.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QueryCache = void 0;
|
|
4
|
-
exports.toCsv = toCsv;
|
|
5
4
|
const node_crypto_1 = require("node:crypto");
|
|
6
5
|
/**
|
|
7
6
|
* A small in-process cache for query results.
|
|
@@ -59,20 +58,3 @@ class QueryCache {
|
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
exports.QueryCache = QueryCache;
|
|
62
|
-
/** CSV, for the export button. */
|
|
63
|
-
function toCsv(result) {
|
|
64
|
-
const escapeCell = (value) => {
|
|
65
|
-
if (value === null || value === undefined)
|
|
66
|
-
return '';
|
|
67
|
-
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
68
|
-
// Quote when the value could otherwise break the row apart. Doubling the
|
|
69
|
-
// quote is the CSV escape, not a backslash.
|
|
70
|
-
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
71
|
-
};
|
|
72
|
-
const lines = [result.columns.map(escapeCell).join(',')];
|
|
73
|
-
for (const row of result.rows) {
|
|
74
|
-
lines.push(result.columns.map((column) => escapeCell(row[column])).join(','));
|
|
75
|
-
}
|
|
76
|
-
// CRLF: Excel still treats a bare LF file as one long row in some locales.
|
|
77
|
-
return `${lines.join('\r\n')}\r\n`;
|
|
78
|
-
}
|
package/dist/catalog.query.d.ts
CHANGED
|
@@ -52,6 +52,27 @@ export interface CatalogQueryRelation {
|
|
|
52
52
|
type: string;
|
|
53
53
|
}>;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The same statement, asked for a row at a time.
|
|
57
|
+
*
|
|
58
|
+
* Two fields differ from {@link CatalogQueryRequest} and both differ because the
|
|
59
|
+
* caller is an export rather than a screen.
|
|
60
|
+
*
|
|
61
|
+
* `maxRows` is optional here and **absent means every row**. A table is a page
|
|
62
|
+
* of a result and a cap is what makes the page; an export is the whole thing by
|
|
63
|
+
* definition, and a capped export is a prefix presented as a file.
|
|
64
|
+
*
|
|
65
|
+
* `timeoutMs` is optional for the matching reason. A statement backing a screen
|
|
66
|
+
* has to answer while somebody waits; an export of a large table legitimately
|
|
67
|
+
* runs for minutes, and the bound that matters for it is that no stage holds
|
|
68
|
+
* more than a row — which is what the stream is for. An implementation given no
|
|
69
|
+
* timeout should impose none of its own.
|
|
70
|
+
*/
|
|
71
|
+
export interface CatalogQueryStreamRequest {
|
|
72
|
+
sql: string;
|
|
73
|
+
maxRows?: number;
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
}
|
|
55
76
|
export interface CatalogQueryStore {
|
|
56
77
|
/**
|
|
57
78
|
* Run a read-only statement.
|
|
@@ -63,8 +84,30 @@ export interface CatalogQueryStore {
|
|
|
63
84
|
runQuery(request: CatalogQueryRequest): Promise<CatalogQueryResult>;
|
|
64
85
|
/** What a query may select from. */
|
|
65
86
|
queryRelations(): Promise<CatalogQueryRelation[]>;
|
|
87
|
+
/**
|
|
88
|
+
* The same read, handing rows over as the engine produces them.
|
|
89
|
+
*
|
|
90
|
+
* **Optional, and the option is the store's to take rather than the caller's.**
|
|
91
|
+
* A store fronting an API, or one on a driver that buffers its result set
|
|
92
|
+
* before resolving, cannot offer this honestly, and a shim that collected the
|
|
93
|
+
* rows and yielded them back would satisfy the type while doing the exact
|
|
94
|
+
* thing the type exists to avoid. So an absent `streamQuery` is a real answer,
|
|
95
|
+
* and {@link CatalogService.streamSavedQuery} falls back to the capped
|
|
96
|
+
* buffered read for it — see the note there about what that costs.
|
|
97
|
+
*
|
|
98
|
+
* The contract on an implementation is one sentence: **do not read ahead of
|
|
99
|
+
* the consumer.** Whatever the driver offers must pause when the consumer
|
|
100
|
+
* stops pulling, all the way to the socket, or the memory has only moved.
|
|
101
|
+
*
|
|
102
|
+
* Returned synchronously — an async generator, not a promise for one — so that
|
|
103
|
+
* a consumer's `for await` owns the resource from the first pull, and an
|
|
104
|
+
* abandoned iteration runs the generator's `finally`.
|
|
105
|
+
*/
|
|
106
|
+
streamQuery?(request: CatalogQueryStreamRequest): AsyncIterable<Record<string, unknown>>;
|
|
66
107
|
}
|
|
67
108
|
export declare function isQueryStore(store: unknown): store is CatalogQueryStore;
|
|
109
|
+
/** A query store that can hand rows over without materialising the result set. */
|
|
110
|
+
export declare function isStreamingQueryStore(store: unknown): store is CatalogQueryStore & Required<Pick<CatalogQueryStore, 'streamQuery'>>;
|
|
68
111
|
/**
|
|
69
112
|
* A cheap sanity check on the shape of a statement.
|
|
70
113
|
*
|
package/dist/catalog.query.js
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.isQueryStore = isQueryStore;
|
|
11
|
+
exports.isStreamingQueryStore = isStreamingQueryStore;
|
|
11
12
|
exports.assertReadOnlyShape = assertReadOnlyShape;
|
|
12
13
|
function isQueryStore(store) {
|
|
13
14
|
return (typeof store === 'object' &&
|
|
14
15
|
store !== null &&
|
|
15
16
|
typeof Reflect.get(store, 'runQuery') === 'function');
|
|
16
17
|
}
|
|
18
|
+
/** A query store that can hand rows over without materialising the result set. */
|
|
19
|
+
function isStreamingQueryStore(store) {
|
|
20
|
+
return isQueryStore(store) && typeof Reflect.get(store, 'streamQuery') === 'function';
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
23
|
* A cheap sanity check on the shape of a statement.
|
|
19
24
|
*
|
|
@@ -22,6 +22,7 @@ export declare class CatalogService {
|
|
|
22
22
|
private readonly workspace?;
|
|
23
23
|
constructor(registry: CatalogRegistry, store: CatalogReadStore, options: CatalogModuleOptions, workspace?: CatalogWorkspaceStore | undefined);
|
|
24
24
|
private readonly cache;
|
|
25
|
+
private readonly logger;
|
|
25
26
|
/** The whole model, as data. */
|
|
26
27
|
getSnapshot(): CatalogSnapshot;
|
|
27
28
|
getType(name: string): CatalogObjectTypeDef | undefined;
|
|
@@ -63,6 +64,21 @@ export declare class CatalogService {
|
|
|
63
64
|
readObjects(typeName: string, query: CatalogObjectQuery & {
|
|
64
65
|
snapshot?: string;
|
|
65
66
|
}): Promise<CatalogObjectPage>;
|
|
67
|
+
/** What the mounted store can push into a read predicate. Empty when it cannot. */
|
|
68
|
+
private filterOperators;
|
|
69
|
+
/**
|
|
70
|
+
* Every filter, or a refusal naming all of them at once.
|
|
71
|
+
*
|
|
72
|
+
* One message listing every problem rather than the first: somebody who built
|
|
73
|
+
* four filters and got two of them wrong should learn that in one round trip.
|
|
74
|
+
*
|
|
75
|
+
* The store is asked whether it can honour the operators before the read runs,
|
|
76
|
+
* which is what stops a store that does not filter from answering with an
|
|
77
|
+
* unfiltered page. That refusal is worth more than it costs — a screen only
|
|
78
|
+
* offers what `filterOperators` reported, so a caller reaching this branch is
|
|
79
|
+
* one that built the request itself.
|
|
80
|
+
*/
|
|
81
|
+
private resolveFilters;
|
|
66
82
|
/** Empty when the store keeps no history. */
|
|
67
83
|
listSnapshots(typeName: string): Promise<SnapshotRef[]>;
|
|
68
84
|
/** What the mounted store can do — the screens branch on this. */
|
|
@@ -117,6 +133,41 @@ export declare class CatalogService {
|
|
|
117
133
|
savedQuery: SavedQuery;
|
|
118
134
|
result: CatalogQueryResult;
|
|
119
135
|
}>;
|
|
136
|
+
/**
|
|
137
|
+
* The same saved query, as rows arriving rather than a result.
|
|
138
|
+
*
|
|
139
|
+
* For the export route, and it differs from {@link runSavedQuery} in three
|
|
140
|
+
* ways that are all the same decision seen from different sides.
|
|
141
|
+
*
|
|
142
|
+
* **No cap when the store streams.** An export is the whole result by
|
|
143
|
+
* definition — that is what distinguishes it from the table it was exported
|
|
144
|
+
* from — so `maxQueryRows` is not applied. It cannot be: a capped export is a
|
|
145
|
+
* prefix handed over as a complete file, with nothing in the file to say so.
|
|
146
|
+
*
|
|
147
|
+
* **No cache, in either direction.** Nothing is read from it, because what it
|
|
148
|
+
* holds is a *capped* page and serving that would silently truncate; and
|
|
149
|
+
* nothing is written to it, because the thing being produced is the object the
|
|
150
|
+
* cache exists to avoid holding.
|
|
151
|
+
*
|
|
152
|
+
* **No timeout.** {@link CatalogModuleOptions.queryTimeoutMs} bounds a
|
|
153
|
+
* statement somebody is waiting on behind a screen. An export of a large table
|
|
154
|
+
* runs for as long as the table takes, and the bound that matters is that
|
|
155
|
+
* neither this process nor the driver holds more than a row — which the stream
|
|
156
|
+
* is what provides. A client that gives up closes the connection, and the
|
|
157
|
+
* consumer stopping its pull is what releases the read.
|
|
158
|
+
*
|
|
159
|
+
* **A store that cannot stream falls back to the capped buffered read**, and
|
|
160
|
+
* that is a real difference in what the same route returns depending on what
|
|
161
|
+
* is mounted underneath. It is the honest option: lifting the cap on a store
|
|
162
|
+
* that materialises its result set would not make the export complete, it
|
|
163
|
+
* would move the failure into the driver, where there is no cap to report. The
|
|
164
|
+
* truncation is logged, since a CSV has nowhere to carry the fact.
|
|
165
|
+
*/
|
|
166
|
+
streamSavedQuery(id: string): Promise<{
|
|
167
|
+
savedQuery: SavedQuery;
|
|
168
|
+
columns?: string[];
|
|
169
|
+
rows: AsyncIterable<Record<string, unknown>>;
|
|
170
|
+
}>;
|
|
120
171
|
listDashboards(): Promise<Dashboard[]>;
|
|
121
172
|
getDashboard(id: string): Promise<Dashboard>;
|
|
122
173
|
/**
|