tmql-orchestration 0.4.1
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/LICENSE +57 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/model/TMModel.d.ts +162 -0
- package/dist/model/TMModel.d.ts.map +1 -0
- package/dist/model/TMModel.js +167 -0
- package/dist/project/TMProject.d.ts +141 -0
- package/dist/project/TMProject.d.ts.map +1 -0
- package/dist/project/TMProject.js +432 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
## Elastic License 2.0 (ELv2)
|
|
2
|
+
|
|
3
|
+
### Acceptance
|
|
4
|
+
|
|
5
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
6
|
+
|
|
7
|
+
### Copyright License
|
|
8
|
+
|
|
9
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below.
|
|
10
|
+
|
|
11
|
+
### Limitations
|
|
12
|
+
|
|
13
|
+
You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
|
|
14
|
+
|
|
15
|
+
You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
|
|
16
|
+
|
|
17
|
+
You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor's trademarks is subject to applicable law.
|
|
18
|
+
|
|
19
|
+
### Patents
|
|
20
|
+
|
|
21
|
+
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
22
|
+
|
|
23
|
+
### Notices
|
|
24
|
+
|
|
25
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
|
|
26
|
+
|
|
27
|
+
If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
|
|
28
|
+
|
|
29
|
+
### No Other Rights
|
|
30
|
+
|
|
31
|
+
These terms do not imply any licenses other than those expressly granted in these terms.
|
|
32
|
+
|
|
33
|
+
### Termination
|
|
34
|
+
|
|
35
|
+
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
|
|
36
|
+
|
|
37
|
+
### No Liability
|
|
38
|
+
|
|
39
|
+
_As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim._
|
|
40
|
+
|
|
41
|
+
### Definitions
|
|
42
|
+
|
|
43
|
+
The **licensor** is the entity offering these terms, and the **software** is the software the licensor makes available under these terms, including any portion of it.
|
|
44
|
+
|
|
45
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
46
|
+
|
|
47
|
+
**your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
48
|
+
|
|
49
|
+
**your licenses** are all the licenses granted to you for the software under these terms.
|
|
50
|
+
|
|
51
|
+
**use** means anything you do with the software requiring one of your licenses.
|
|
52
|
+
|
|
53
|
+
**trademark** means trademarks, service marks, and similar rights.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
Copyright (c) 2026 Ryan Peggs & Tim Vyas. All rights reserved.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmql-orchestration
|
|
3
|
+
*
|
|
4
|
+
* DAG composition and orchestration layer for tmql.
|
|
5
|
+
* Provides TMModel and TMProject for building data pipelines.
|
|
6
|
+
*/
|
|
7
|
+
export { TMModel, isTMModel } from "./model/TMModel";
|
|
8
|
+
export { TMProject } from "./project/TMProject";
|
|
9
|
+
export type { InferModelOutput } from "./model/TMModel";
|
|
10
|
+
export type { MaterializeConfig, MergeOptions, CollectionMode, TypedTimeSeriesOptions, ModelConfig, } from "./model/TMModel";
|
|
11
|
+
export type { ProjectConfig, RunOptions, ModelRunStats, ProjectRunResult, ExecutionPlan, ValidationResult, ValidationError, ValidationWarning, } from "./project/TMProject";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,WAAW,GACZ,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,aAAa,EACb,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAClB,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmql-orchestration
|
|
3
|
+
*
|
|
4
|
+
* DAG composition and orchestration layer for tmql.
|
|
5
|
+
* Provides TMModel and TMProject for building data pipelines.
|
|
6
|
+
*/
|
|
7
|
+
// DAG Composition
|
|
8
|
+
export { TMModel, isTMModel } from "./model/TMModel";
|
|
9
|
+
export { TMProject } from "./project/TMProject";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMModel - DAG Pipeline Composition
|
|
3
|
+
*
|
|
4
|
+
* A model represents a named, materializable pipeline with typed input/output.
|
|
5
|
+
* Models can depend on collections or other models, forming a DAG.
|
|
6
|
+
*/
|
|
7
|
+
import { TimeSeriesCollectionOptions } from "mongodb";
|
|
8
|
+
import { Document, TMPipeline, TMSource, InferSourceType, FieldSelector, FieldSelectorsThatInferTo, TopLevelField } from "tmql";
|
|
9
|
+
/**
|
|
10
|
+
* Type-safe extension of MongoDB's TimeSeriesCollectionOptions.
|
|
11
|
+
* Constrains timeField to Date fields and metaField to string keys.
|
|
12
|
+
*/
|
|
13
|
+
export type TypedTimeSeriesOptions<TDoc extends Document> = Omit<TimeSeriesCollectionOptions, "timeField" | "metaField"> & {
|
|
14
|
+
/** Must be a field of type Date in TDoc */
|
|
15
|
+
timeField: TopLevelField<FieldSelectorsThatInferTo<TDoc, Date>>;
|
|
16
|
+
/** Must be a string key in TDoc */
|
|
17
|
+
metaField?: TopLevelField<FieldSelector<TDoc>>;
|
|
18
|
+
/** TTL - auto-expire documents after N seconds */
|
|
19
|
+
expireAfterSeconds?: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Type-safe $merge stage options.
|
|
23
|
+
*/
|
|
24
|
+
type TopLevelFieldOf<T extends Document> = TopLevelField<FieldSelector<T>>;
|
|
25
|
+
export type MergeOptions<TOutput extends Document> = {
|
|
26
|
+
/** Field(s) to match on - must exist in output document */
|
|
27
|
+
on: TopLevelFieldOf<TOutput> | TopLevelFieldOf<TOutput>[];
|
|
28
|
+
/** Action when document matches existing */
|
|
29
|
+
whenMatched?: "replace" | "merge" | "keepExisting" | "fail";
|
|
30
|
+
/** Action when document doesn't match existing */
|
|
31
|
+
whenNotMatched?: "insert" | "discard" | "fail";
|
|
32
|
+
};
|
|
33
|
+
export type CollectionMode<TOutput extends Document> = {
|
|
34
|
+
$out: Record<string, never>;
|
|
35
|
+
} | {
|
|
36
|
+
$merge: MergeOptions<TOutput>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Materialization configuration.
|
|
40
|
+
*/
|
|
41
|
+
export type MaterializeConfig<TOutput extends Document = Document> = {
|
|
42
|
+
type: "view";
|
|
43
|
+
db?: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: "collection";
|
|
46
|
+
db?: string;
|
|
47
|
+
mode: CollectionMode<TOutput>;
|
|
48
|
+
timeseries?: TypedTimeSeriesOptions<TOutput>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Model configuration - source can be a collection OR another model.
|
|
52
|
+
* When source is a model, this creates an implicit DAG edge.
|
|
53
|
+
*
|
|
54
|
+
* Pipelines inside models use `mode: "model"` which allows lookup/unionWith
|
|
55
|
+
* from other TMModels (not just TMCollections).
|
|
56
|
+
*/
|
|
57
|
+
export type ModelConfig<TName extends string, TSource extends TMSource<Document>, TOutput extends Document, TMaterializeConfig extends MaterializeConfig<TOutput>> = {
|
|
58
|
+
name: TName;
|
|
59
|
+
/** Source collection or upstream model */
|
|
60
|
+
from: TSource;
|
|
61
|
+
/** Pipeline function - receives a "model" mode pipeline that can lookup from other models */
|
|
62
|
+
pipeline: (p: TMPipeline<InferSourceType<TSource>, InferSourceType<TSource>, "model">) => TMPipeline<InferSourceType<TSource>, TOutput, "model">;
|
|
63
|
+
/** Materialization configuration - required */
|
|
64
|
+
materialize: TMaterializeConfig;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Extract the output document type from a TMModel.
|
|
68
|
+
*/
|
|
69
|
+
export type InferModelOutput<T> = T extends TMModel<any, any, infer O, any> ? O : never;
|
|
70
|
+
/**
|
|
71
|
+
* Type predicate to check if a value is a TMModel.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isTMModel(value: unknown): value is TMModel;
|
|
74
|
+
/**
|
|
75
|
+
* A named, materializable pipeline with typed input/output.
|
|
76
|
+
* Models form a DAG through their `from` property.
|
|
77
|
+
*/
|
|
78
|
+
export declare class TMModel<TName extends string = string, TInput extends Document = Document, TOutput extends Document = Document, TMaterializeConfig extends MaterializeConfig<TOutput> = MaterializeConfig<TOutput>> implements TMSource<TOutput> {
|
|
79
|
+
/** Preset modes for common materialization patterns */
|
|
80
|
+
static readonly Mode: {
|
|
81
|
+
/** Replace entire collection using $out */
|
|
82
|
+
readonly Replace: {
|
|
83
|
+
readonly $out: {};
|
|
84
|
+
};
|
|
85
|
+
/** Upsert by _id using $merge */
|
|
86
|
+
readonly Upsert: {
|
|
87
|
+
readonly $merge: {
|
|
88
|
+
readonly on: "_id";
|
|
89
|
+
readonly whenMatched: "replace";
|
|
90
|
+
readonly whenNotMatched: "insert";
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/** Append only (fail on match) using $merge */
|
|
94
|
+
readonly Append: {
|
|
95
|
+
readonly $merge: {
|
|
96
|
+
readonly on: "_id";
|
|
97
|
+
readonly whenMatched: "fail";
|
|
98
|
+
readonly whenNotMatched: "insert";
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
/** TMSource discriminator */
|
|
103
|
+
readonly sourceType: "model";
|
|
104
|
+
readonly name: TName;
|
|
105
|
+
readonly materialize: TMaterializeConfig;
|
|
106
|
+
readonly __inputType: TInput;
|
|
107
|
+
readonly __outputType: TOutput;
|
|
108
|
+
private readonly _from;
|
|
109
|
+
private readonly _pipelineFn;
|
|
110
|
+
constructor(config: ModelConfig<TName, TMSource<TInput>, TOutput, TMaterializeConfig>);
|
|
111
|
+
/**
|
|
112
|
+
* Get the source (collection or upstream model).
|
|
113
|
+
*/
|
|
114
|
+
getSource(): TMSource<TInput>;
|
|
115
|
+
/**
|
|
116
|
+
* Check if the source is a model (vs a collection).
|
|
117
|
+
*/
|
|
118
|
+
sourceIsModel(): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Get the upstream model if source is a model, otherwise undefined.
|
|
121
|
+
*/
|
|
122
|
+
getUpstreamModel(): TMModel<string, any, TInput, any> | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Get the source collection name.
|
|
125
|
+
*/
|
|
126
|
+
getSourceCollectionName(): string;
|
|
127
|
+
/**
|
|
128
|
+
* Get the source database name.
|
|
129
|
+
*/
|
|
130
|
+
getSourceDatabase(): string | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Get the output collection name (where this model materializes).
|
|
133
|
+
*/
|
|
134
|
+
getOutputCollectionName(): string;
|
|
135
|
+
/**
|
|
136
|
+
* Get the database name for output (if specified).
|
|
137
|
+
*/
|
|
138
|
+
getOutputDatabase(): string | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* Build the pipeline stages for this model.
|
|
141
|
+
*/
|
|
142
|
+
getPipelineStages(): Document[];
|
|
143
|
+
/**
|
|
144
|
+
* Get ancestor sources from lookup/unionWith stages.
|
|
145
|
+
* These are sources referenced in the pipeline but not via the `from` property.
|
|
146
|
+
*/
|
|
147
|
+
getAncestorsFromStages(): TMSource<any>[];
|
|
148
|
+
/**
|
|
149
|
+
* Internal: build the pipeline and return the TMPipeline instance.
|
|
150
|
+
*/
|
|
151
|
+
private _buildPipeline;
|
|
152
|
+
/**
|
|
153
|
+
* Build the complete aggregation pipeline including output stage.
|
|
154
|
+
*/
|
|
155
|
+
buildPipeline(): Document[];
|
|
156
|
+
/**
|
|
157
|
+
* Build the $out or $merge stage based on materialization config.
|
|
158
|
+
*/
|
|
159
|
+
private buildOutputStage;
|
|
160
|
+
}
|
|
161
|
+
export {};
|
|
162
|
+
//# sourceMappingURL=TMModel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TMModel.d.ts","sourceRoot":"","sources":["../../src/model/TMModel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMModel - DAG Pipeline Composition
|
|
3
|
+
*
|
|
4
|
+
* A model represents a named, materializable pipeline with typed input/output.
|
|
5
|
+
* Models can depend on collections or other models, forming a DAG.
|
|
6
|
+
*/
|
|
7
|
+
import { TMPipeline, } from "tmql";
|
|
8
|
+
/**
|
|
9
|
+
* Type predicate to check if a value is a TMModel.
|
|
10
|
+
*/
|
|
11
|
+
export function isTMModel(value) {
|
|
12
|
+
return (typeof value === "object" &&
|
|
13
|
+
value !== null &&
|
|
14
|
+
"sourceType" in value &&
|
|
15
|
+
value.sourceType === "model");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A named, materializable pipeline with typed input/output.
|
|
19
|
+
* Models form a DAG through their `from` property.
|
|
20
|
+
*/
|
|
21
|
+
export class TMModel {
|
|
22
|
+
/** Preset modes for common materialization patterns */
|
|
23
|
+
static Mode = {
|
|
24
|
+
/** Replace entire collection using $out */
|
|
25
|
+
Replace: { $out: {} },
|
|
26
|
+
/** Upsert by _id using $merge */
|
|
27
|
+
Upsert: {
|
|
28
|
+
$merge: { on: "_id", whenMatched: "replace", whenNotMatched: "insert" },
|
|
29
|
+
},
|
|
30
|
+
/** Append only (fail on match) using $merge */
|
|
31
|
+
Append: {
|
|
32
|
+
$merge: { on: "_id", whenMatched: "fail", whenNotMatched: "insert" },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** TMSource discriminator */
|
|
36
|
+
sourceType = "model";
|
|
37
|
+
name;
|
|
38
|
+
materialize;
|
|
39
|
+
// Phantom types for inference (not used at runtime)
|
|
40
|
+
__inputType;
|
|
41
|
+
__outputType;
|
|
42
|
+
_from;
|
|
43
|
+
_pipelineFn;
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.name = config.name;
|
|
46
|
+
this._from = config.from;
|
|
47
|
+
this._pipelineFn = config.pipeline;
|
|
48
|
+
this.materialize = config.materialize;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the source (collection or upstream model).
|
|
52
|
+
*/
|
|
53
|
+
getSource() {
|
|
54
|
+
return this._from;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if the source is a model (vs a collection).
|
|
58
|
+
*/
|
|
59
|
+
sourceIsModel() {
|
|
60
|
+
return isTMModel(this._from);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the upstream model if source is a model, otherwise undefined.
|
|
64
|
+
*/
|
|
65
|
+
getUpstreamModel() {
|
|
66
|
+
if (this.sourceIsModel()) {
|
|
67
|
+
return this._from;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the source collection name.
|
|
73
|
+
*/
|
|
74
|
+
getSourceCollectionName() {
|
|
75
|
+
return this._from.getOutputCollectionName();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the source database name.
|
|
79
|
+
*/
|
|
80
|
+
getSourceDatabase() {
|
|
81
|
+
return this._from.getOutputDatabase();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get the output collection name (where this model materializes).
|
|
85
|
+
*/
|
|
86
|
+
getOutputCollectionName() {
|
|
87
|
+
return this.name;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get the database name for output (if specified).
|
|
91
|
+
*/
|
|
92
|
+
getOutputDatabase() {
|
|
93
|
+
if ("db" in this.materialize) {
|
|
94
|
+
return this.materialize.db;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build the pipeline stages for this model.
|
|
100
|
+
*/
|
|
101
|
+
getPipelineStages() {
|
|
102
|
+
return this._buildPipeline().getPipeline();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get ancestor sources from lookup/unionWith stages.
|
|
106
|
+
* These are sources referenced in the pipeline but not via the `from` property.
|
|
107
|
+
*/
|
|
108
|
+
getAncestorsFromStages() {
|
|
109
|
+
return this._buildPipeline().getAncestorsFromStages();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Internal: build the pipeline and return the TMPipeline instance.
|
|
113
|
+
*/
|
|
114
|
+
_buildPipeline() {
|
|
115
|
+
// Start with an empty "model" mode pipeline (allows lookup from other models)
|
|
116
|
+
const startPipeline = new TMPipeline({
|
|
117
|
+
pipeline: [],
|
|
118
|
+
});
|
|
119
|
+
// Execute pipeline function
|
|
120
|
+
return this._pipelineFn(startPipeline);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build the complete aggregation pipeline including output stage.
|
|
124
|
+
*/
|
|
125
|
+
buildPipeline() {
|
|
126
|
+
const stages = this.getPipelineStages();
|
|
127
|
+
// Add output stage based on materialization
|
|
128
|
+
const outputStage = this.buildOutputStage();
|
|
129
|
+
if (outputStage) {
|
|
130
|
+
return [...stages, outputStage];
|
|
131
|
+
}
|
|
132
|
+
return stages;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the $out or $merge stage based on materialization config.
|
|
136
|
+
*/
|
|
137
|
+
buildOutputStage() {
|
|
138
|
+
const outputCollection = this.getOutputCollectionName();
|
|
139
|
+
const outputDb = this.getOutputDatabase();
|
|
140
|
+
if (this.materialize.type === "view") {
|
|
141
|
+
// Views are created separately, not via pipeline
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (this.materialize.type === "collection") {
|
|
145
|
+
const mode = this.materialize.mode;
|
|
146
|
+
if ("$out" in mode) {
|
|
147
|
+
return outputDb ?
|
|
148
|
+
{ $out: { db: outputDb, coll: outputCollection } }
|
|
149
|
+
: { $out: outputCollection };
|
|
150
|
+
}
|
|
151
|
+
if ("$merge" in mode) {
|
|
152
|
+
const into = outputDb ?
|
|
153
|
+
{ db: outputDb, coll: outputCollection }
|
|
154
|
+
: outputCollection;
|
|
155
|
+
return {
|
|
156
|
+
$merge: {
|
|
157
|
+
into,
|
|
158
|
+
on: mode.$merge.on,
|
|
159
|
+
whenMatched: mode.$merge.whenMatched ?? "replace",
|
|
160
|
+
whenNotMatched: mode.$merge.whenNotMatched ?? "insert",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMProject - DAG Orchestrator for TMModels
|
|
3
|
+
*
|
|
4
|
+
* Manages a collection of models, resolves dependencies,
|
|
5
|
+
* and executes them in topological order.
|
|
6
|
+
*/
|
|
7
|
+
import { MongoClient } from "mongodb";
|
|
8
|
+
import { TMModel } from "../model/TMModel";
|
|
9
|
+
export type ProjectConfig = {
|
|
10
|
+
name: string;
|
|
11
|
+
models: TMModel<any, any, any, any>[];
|
|
12
|
+
defaultDatabase?: string;
|
|
13
|
+
};
|
|
14
|
+
export type RunOptions = {
|
|
15
|
+
/** Specific models to run (default: all) */
|
|
16
|
+
targets?: string[];
|
|
17
|
+
/** Models to exclude */
|
|
18
|
+
exclude?: string[];
|
|
19
|
+
/** Log plan without executing */
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
/** MongoDB client (falls back to tmql singleton) */
|
|
22
|
+
client?: MongoClient;
|
|
23
|
+
/** Database name (falls back to project default) */
|
|
24
|
+
databaseName?: string;
|
|
25
|
+
/** Callbacks */
|
|
26
|
+
onModelStart?: (name: string) => void;
|
|
27
|
+
onModelComplete?: (name: string, stats: ModelRunStats) => void;
|
|
28
|
+
onModelError?: (name: string, error: Error) => void;
|
|
29
|
+
};
|
|
30
|
+
export type ModelRunStats = {
|
|
31
|
+
durationMs: number;
|
|
32
|
+
};
|
|
33
|
+
export type ProjectRunResult = {
|
|
34
|
+
success: boolean;
|
|
35
|
+
modelsRun: string[];
|
|
36
|
+
modelsFailed: string[];
|
|
37
|
+
stats: Record<string, ModelRunStats>;
|
|
38
|
+
totalDurationMs: number;
|
|
39
|
+
};
|
|
40
|
+
export type ExecutionPlan = {
|
|
41
|
+
/** Models grouped by parallel execution stage */
|
|
42
|
+
stages: string[][];
|
|
43
|
+
/** Total models in plan */
|
|
44
|
+
totalModels: number;
|
|
45
|
+
/** Render as Mermaid diagram */
|
|
46
|
+
toMermaid(): string;
|
|
47
|
+
/** Render as text */
|
|
48
|
+
toString(): string;
|
|
49
|
+
};
|
|
50
|
+
export type ValidationResult = {
|
|
51
|
+
valid: boolean;
|
|
52
|
+
errors: ValidationError[];
|
|
53
|
+
warnings: ValidationWarning[];
|
|
54
|
+
};
|
|
55
|
+
export type ValidationError = {
|
|
56
|
+
type: "cycle" | "missing_ref" | "duplicate_name";
|
|
57
|
+
message: string;
|
|
58
|
+
models: string[];
|
|
59
|
+
};
|
|
60
|
+
export type ValidationWarning = {
|
|
61
|
+
type: "orphan";
|
|
62
|
+
message: string;
|
|
63
|
+
models: string[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* TMProject - DAG orchestrator for TMModels.
|
|
67
|
+
*
|
|
68
|
+
* Models are provided at construction time and validated immediately.
|
|
69
|
+
* The project is immutable after creation.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* import { stgEvents, dailyMetrics } from "./models/analytics";
|
|
74
|
+
*
|
|
75
|
+
* const analyticsProject = new TMProject({
|
|
76
|
+
* name: "analytics",
|
|
77
|
+
* models: [stgEvents, dailyMetrics],
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* // See execution plan
|
|
81
|
+
* console.log(analyticsProject.toMermaid());
|
|
82
|
+
*
|
|
83
|
+
* // Run all models
|
|
84
|
+
* await analyticsProject.run();
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare class TMProject {
|
|
88
|
+
readonly name: string;
|
|
89
|
+
private readonly defaultDatabase;
|
|
90
|
+
private readonly models;
|
|
91
|
+
constructor(config: ProjectConfig);
|
|
92
|
+
/**
|
|
93
|
+
* Get a model by name.
|
|
94
|
+
*/
|
|
95
|
+
getModel<TName extends string>(name: TName): TMModel<TName, any, any, any> | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Get all models.
|
|
98
|
+
*/
|
|
99
|
+
getModels(): TMModel<any, any, any, any>[];
|
|
100
|
+
/**
|
|
101
|
+
* Build an execution plan (topological sort).
|
|
102
|
+
*/
|
|
103
|
+
plan(options?: Pick<RunOptions, "targets" | "exclude">): ExecutionPlan;
|
|
104
|
+
/**
|
|
105
|
+
* Validate the DAG (check for cycles, missing refs, etc.).
|
|
106
|
+
*/
|
|
107
|
+
validate(): ValidationResult;
|
|
108
|
+
/**
|
|
109
|
+
* Execute the DAG (or subset of models).
|
|
110
|
+
*/
|
|
111
|
+
run(options?: RunOptions): Promise<ProjectRunResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Execute a single model.
|
|
114
|
+
*/
|
|
115
|
+
private executeModel;
|
|
116
|
+
/**
|
|
117
|
+
* Render the DAG as a Mermaid diagram.
|
|
118
|
+
*/
|
|
119
|
+
toMermaid(): string;
|
|
120
|
+
/**
|
|
121
|
+
* Get models to run based on targets and exclusions.
|
|
122
|
+
*/
|
|
123
|
+
private getModelsToRun;
|
|
124
|
+
/**
|
|
125
|
+
* Build dependency graph: { modelName: [dependencies] }
|
|
126
|
+
*/
|
|
127
|
+
private buildDependencyGraph;
|
|
128
|
+
/**
|
|
129
|
+
* Topological sort into parallel stages.
|
|
130
|
+
*/
|
|
131
|
+
private topologicalSort;
|
|
132
|
+
/**
|
|
133
|
+
* Detect cycles in the dependency graph.
|
|
134
|
+
*/
|
|
135
|
+
private detectCycle;
|
|
136
|
+
/**
|
|
137
|
+
* Render dependency graph as Mermaid.
|
|
138
|
+
*/
|
|
139
|
+
private toMermaidFromDeps;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=TMProject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TMProject.d.ts","sourceRoot":"","sources":["../../src/project/TMProject.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMProject - DAG Orchestrator for TMModels
|
|
3
|
+
*
|
|
4
|
+
* Manages a collection of models, resolves dependencies,
|
|
5
|
+
* and executes them in topological order.
|
|
6
|
+
*/
|
|
7
|
+
import { tmql } from "tmql";
|
|
8
|
+
import { isTMModel } from "../model/TMModel";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// TMProject Class
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* TMProject - DAG orchestrator for TMModels.
|
|
14
|
+
*
|
|
15
|
+
* Models are provided at construction time and validated immediately.
|
|
16
|
+
* The project is immutable after creation.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { stgEvents, dailyMetrics } from "./models/analytics";
|
|
21
|
+
*
|
|
22
|
+
* const analyticsProject = new TMProject({
|
|
23
|
+
* name: "analytics",
|
|
24
|
+
* models: [stgEvents, dailyMetrics],
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // See execution plan
|
|
28
|
+
* console.log(analyticsProject.toMermaid());
|
|
29
|
+
*
|
|
30
|
+
* // Run all models
|
|
31
|
+
* await analyticsProject.run();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class TMProject {
|
|
35
|
+
name;
|
|
36
|
+
defaultDatabase;
|
|
37
|
+
models;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.name = config.name;
|
|
40
|
+
this.defaultDatabase =
|
|
41
|
+
config.defaultDatabase !== undefined ? config.defaultDatabase : undefined;
|
|
42
|
+
// Build models map, auto-discovering all ancestors (including lookup and unionWith)
|
|
43
|
+
this.models = new Map();
|
|
44
|
+
const addModelWithAncestors = (model) => {
|
|
45
|
+
if (this.models.has(model.name))
|
|
46
|
+
return;
|
|
47
|
+
// First add upstream ancestor (from property)
|
|
48
|
+
const upstream = model.getUpstreamModel();
|
|
49
|
+
if (upstream) {
|
|
50
|
+
addModelWithAncestors(upstream);
|
|
51
|
+
}
|
|
52
|
+
// Then add ancestors from lookup/unionWith stages (filter to models only)
|
|
53
|
+
for (const ancestor of model.getAncestorsFromStages().filter(isTMModel)) {
|
|
54
|
+
addModelWithAncestors(ancestor);
|
|
55
|
+
}
|
|
56
|
+
// Finally add this model
|
|
57
|
+
this.models.set(model.name, model);
|
|
58
|
+
};
|
|
59
|
+
for (const model of config.models) {
|
|
60
|
+
addModelWithAncestors(model);
|
|
61
|
+
}
|
|
62
|
+
// Validate immediately - fail fast
|
|
63
|
+
const validation = this.validate();
|
|
64
|
+
if (!validation.valid) {
|
|
65
|
+
const errorMessages = validation.errors
|
|
66
|
+
.map((e) => ` - ${e.message}`)
|
|
67
|
+
.join("\n");
|
|
68
|
+
throw new Error(`Project "${this.name}" has validation errors:\n${errorMessages}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get a model by name.
|
|
73
|
+
*/
|
|
74
|
+
getModel(name) {
|
|
75
|
+
return this.models.get(name);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get all models.
|
|
79
|
+
*/
|
|
80
|
+
getModels() {
|
|
81
|
+
return Array.from(this.models.values());
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build an execution plan (topological sort).
|
|
85
|
+
*/
|
|
86
|
+
plan(options = {}) {
|
|
87
|
+
const { targets, exclude = [] } = options;
|
|
88
|
+
// Get models to run
|
|
89
|
+
const modelsToRun = this.getModelsToRun(targets, exclude);
|
|
90
|
+
// Build dependency graph
|
|
91
|
+
const deps = this.buildDependencyGraph(modelsToRun);
|
|
92
|
+
// Topological sort into stages (parallel batches)
|
|
93
|
+
const stages = this.topologicalSort(deps);
|
|
94
|
+
return {
|
|
95
|
+
stages,
|
|
96
|
+
totalModels: stages.flat().length,
|
|
97
|
+
toMermaid: () => this.toMermaidFromDeps(deps),
|
|
98
|
+
toString: () => stages
|
|
99
|
+
.map((stage, i) => `Stage ${i + 1}: ${stage.join(", ")}`)
|
|
100
|
+
.join("\n"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Validate the DAG (check for cycles, missing refs, etc.).
|
|
105
|
+
*/
|
|
106
|
+
validate() {
|
|
107
|
+
const errors = [];
|
|
108
|
+
const warnings = [];
|
|
109
|
+
// Check for duplicate names
|
|
110
|
+
const names = new Set();
|
|
111
|
+
for (const model of this.models.values()) {
|
|
112
|
+
if (names.has(model.name)) {
|
|
113
|
+
errors.push({
|
|
114
|
+
type: "duplicate_name",
|
|
115
|
+
message: `Duplicate model name: "${model.name}"`,
|
|
116
|
+
models: [model.name],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
names.add(model.name);
|
|
120
|
+
}
|
|
121
|
+
// Check for missing dependencies
|
|
122
|
+
for (const model of this.models.values()) {
|
|
123
|
+
const upstream = model.getUpstreamModel();
|
|
124
|
+
if (upstream && !this.models.has(upstream.name)) {
|
|
125
|
+
errors.push({
|
|
126
|
+
type: "missing_ref",
|
|
127
|
+
message: `Model "${model.name}" depends on "${upstream.name}" which is not in this project`,
|
|
128
|
+
models: [model.name, upstream.name],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Check for cycles
|
|
133
|
+
const cycle = this.detectCycle();
|
|
134
|
+
if (cycle) {
|
|
135
|
+
errors.push({
|
|
136
|
+
type: "cycle",
|
|
137
|
+
message: `Circular dependency detected: ${cycle.join(" -> ")}`,
|
|
138
|
+
models: cycle,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// Check for orphan models (no downstream dependencies)
|
|
142
|
+
const hasDownstream = new Set();
|
|
143
|
+
for (const model of this.models.values()) {
|
|
144
|
+
const upstream = model.getUpstreamModel();
|
|
145
|
+
if (upstream) {
|
|
146
|
+
hasDownstream.add(upstream.name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const orphans = Array.from(this.models.values())
|
|
150
|
+
.filter((m) => !hasDownstream.has(m.name))
|
|
151
|
+
.map((m) => m.name);
|
|
152
|
+
// Only warn if there are multiple orphans (leaf nodes are expected)
|
|
153
|
+
if (orphans.length > 1) {
|
|
154
|
+
warnings.push({
|
|
155
|
+
type: "orphan",
|
|
156
|
+
message: `Multiple leaf models with no downstream dependencies: ${orphans.join(", ")}`,
|
|
157
|
+
models: orphans,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
valid: errors.length === 0,
|
|
162
|
+
errors,
|
|
163
|
+
warnings,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Execute the DAG (or subset of models).
|
|
168
|
+
*/
|
|
169
|
+
async run(options = {}) {
|
|
170
|
+
const startTime = Date.now();
|
|
171
|
+
const { targets, exclude, dryRun = false, client, databaseName, onModelStart, onModelComplete, onModelError, } = options;
|
|
172
|
+
const planOptions = {};
|
|
173
|
+
if (targets !== undefined)
|
|
174
|
+
planOptions.targets = targets;
|
|
175
|
+
if (exclude !== undefined)
|
|
176
|
+
planOptions.exclude = exclude;
|
|
177
|
+
const executionPlan = this.plan(planOptions);
|
|
178
|
+
const modelsRun = [];
|
|
179
|
+
const modelsFailed = [];
|
|
180
|
+
const stats = {};
|
|
181
|
+
if (dryRun) {
|
|
182
|
+
console.log("Dry run - execution plan:");
|
|
183
|
+
console.log(executionPlan.toString());
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
modelsRun: [],
|
|
187
|
+
modelsFailed: [],
|
|
188
|
+
stats: {},
|
|
189
|
+
totalDurationMs: Date.now() - startTime,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Get MongoDB client
|
|
193
|
+
const mongoClient = client ?? tmql.client;
|
|
194
|
+
if (!mongoClient) {
|
|
195
|
+
throw new Error("No MongoDB client available. Either pass one via options.client or call tmql.connect() first.");
|
|
196
|
+
}
|
|
197
|
+
const dbName = databaseName ?? this.defaultDatabase;
|
|
198
|
+
// Execute stages sequentially, models within a stage in parallel
|
|
199
|
+
for (const stage of executionPlan.stages) {
|
|
200
|
+
const stageResults = await Promise.allSettled(stage.map(async (modelName) => {
|
|
201
|
+
const model = this.models.get(modelName);
|
|
202
|
+
const modelStart = Date.now();
|
|
203
|
+
onModelStart?.(modelName);
|
|
204
|
+
try {
|
|
205
|
+
await this.executeModel(model, mongoClient, dbName);
|
|
206
|
+
const modelStats = {
|
|
207
|
+
durationMs: Date.now() - modelStart,
|
|
208
|
+
};
|
|
209
|
+
stats[modelName] = modelStats;
|
|
210
|
+
modelsRun.push(modelName);
|
|
211
|
+
onModelComplete?.(modelName, modelStats);
|
|
212
|
+
return { success: true, modelName };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
216
|
+
modelsFailed.push(modelName);
|
|
217
|
+
onModelError?.(modelName, err);
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
// Check for failures
|
|
222
|
+
const failures = stageResults.filter((r) => r.status === "rejected");
|
|
223
|
+
if (failures.length > 0) {
|
|
224
|
+
// Stop execution on failure
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
success: modelsFailed.length === 0,
|
|
230
|
+
modelsRun,
|
|
231
|
+
modelsFailed,
|
|
232
|
+
stats,
|
|
233
|
+
totalDurationMs: Date.now() - startTime,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Execute a single model.
|
|
238
|
+
*/
|
|
239
|
+
async executeModel(model, client, dbName) {
|
|
240
|
+
// Build pipeline with output stage
|
|
241
|
+
const pipeline = model.buildPipeline();
|
|
242
|
+
// Get source collection
|
|
243
|
+
const sourceCollection = model.getSourceCollectionName();
|
|
244
|
+
const outputDb = model.getOutputDatabase() ?? dbName;
|
|
245
|
+
// Handle view creation separately
|
|
246
|
+
if (model.materialize.type === "view") {
|
|
247
|
+
const viewName = model.getOutputCollectionName();
|
|
248
|
+
const viewDb = client.db(outputDb);
|
|
249
|
+
// Drop existing view if exists
|
|
250
|
+
try {
|
|
251
|
+
await viewDb.dropCollection(viewName);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// View might not exist
|
|
255
|
+
}
|
|
256
|
+
// Create view
|
|
257
|
+
await viewDb.createCollection(viewName, {
|
|
258
|
+
viewOn: sourceCollection,
|
|
259
|
+
pipeline: model.getPipelineStages(),
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Handle time-series collection creation
|
|
264
|
+
if (model.materialize.type === "collection" &&
|
|
265
|
+
"timeseries" in model.materialize &&
|
|
266
|
+
model.materialize.timeseries) {
|
|
267
|
+
const collName = model.getOutputCollectionName();
|
|
268
|
+
const db = client.db(outputDb);
|
|
269
|
+
// Check if collection exists
|
|
270
|
+
const collections = await db
|
|
271
|
+
.listCollections({ name: collName })
|
|
272
|
+
.toArray();
|
|
273
|
+
if (collections.length === 0) {
|
|
274
|
+
// Create time-series collection
|
|
275
|
+
await db.createCollection(collName, {
|
|
276
|
+
timeseries: {
|
|
277
|
+
timeField: model.materialize.timeseries.timeField,
|
|
278
|
+
metaField: model.materialize.timeseries.metaField,
|
|
279
|
+
granularity: model.materialize.timeseries.granularity,
|
|
280
|
+
},
|
|
281
|
+
expireAfterSeconds: model.materialize.timeseries.expireAfterSeconds,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Execute aggregation
|
|
286
|
+
// Use source database for reading, output stage handles writing to correct db
|
|
287
|
+
const sourceDb = model.getSourceDatabase() ?? dbName;
|
|
288
|
+
const db = client.db(sourceDb);
|
|
289
|
+
const collection = db.collection(sourceCollection);
|
|
290
|
+
const cursor = collection.aggregate(pipeline);
|
|
291
|
+
await cursor.toArray();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Render the DAG as a Mermaid diagram.
|
|
295
|
+
*/
|
|
296
|
+
toMermaid() {
|
|
297
|
+
const deps = this.buildDependencyGraph(Array.from(this.models.values()).map((m) => m.name));
|
|
298
|
+
return this.toMermaidFromDeps(deps);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get models to run based on targets and exclusions.
|
|
302
|
+
*/
|
|
303
|
+
getModelsToRun(targets, exclude = []) {
|
|
304
|
+
const excludeSet = new Set(exclude);
|
|
305
|
+
if (targets && targets.length > 0) {
|
|
306
|
+
// Run specified targets and their dependencies
|
|
307
|
+
const toRun = new Set();
|
|
308
|
+
const addWithDeps = (name) => {
|
|
309
|
+
if (toRun.has(name) || excludeSet.has(name))
|
|
310
|
+
return;
|
|
311
|
+
const model = this.models.get(name);
|
|
312
|
+
if (!model) {
|
|
313
|
+
throw new Error(`Target model "${name}" not found in project`);
|
|
314
|
+
}
|
|
315
|
+
toRun.add(name);
|
|
316
|
+
const upstream = model.getUpstreamModel();
|
|
317
|
+
if (upstream) {
|
|
318
|
+
addWithDeps(upstream.name);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
targets.forEach(addWithDeps);
|
|
322
|
+
return Array.from(toRun);
|
|
323
|
+
}
|
|
324
|
+
// Run all models except excluded
|
|
325
|
+
return Array.from(this.models.keys()).filter((name) => !excludeSet.has(name));
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Build dependency graph: { modelName: [dependencies] }
|
|
329
|
+
*/
|
|
330
|
+
buildDependencyGraph(modelNames) {
|
|
331
|
+
const deps = new Map();
|
|
332
|
+
for (const name of modelNames) {
|
|
333
|
+
const model = this.models.get(name);
|
|
334
|
+
if (!model)
|
|
335
|
+
continue;
|
|
336
|
+
const modelDeps = [];
|
|
337
|
+
const upstream = model.getUpstreamModel();
|
|
338
|
+
if (upstream && modelNames.includes(upstream.name)) {
|
|
339
|
+
modelDeps.push(upstream.name);
|
|
340
|
+
}
|
|
341
|
+
deps.set(name, modelDeps);
|
|
342
|
+
}
|
|
343
|
+
return deps;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Topological sort into parallel stages.
|
|
347
|
+
*/
|
|
348
|
+
topologicalSort(deps) {
|
|
349
|
+
const stages = [];
|
|
350
|
+
const remaining = new Map(deps);
|
|
351
|
+
const completed = new Set();
|
|
352
|
+
while (remaining.size > 0) {
|
|
353
|
+
// Find models with all dependencies satisfied
|
|
354
|
+
const ready = [];
|
|
355
|
+
for (const [name, modelDeps] of remaining) {
|
|
356
|
+
if (modelDeps.every((dep) => completed.has(dep))) {
|
|
357
|
+
ready.push(name);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (ready.length === 0 && remaining.size > 0) {
|
|
361
|
+
throw new Error(`Circular dependency detected among: ${Array.from(remaining.keys()).join(", ")}`);
|
|
362
|
+
}
|
|
363
|
+
// Add to current stage
|
|
364
|
+
stages.push(ready);
|
|
365
|
+
// Mark as completed
|
|
366
|
+
for (const name of ready) {
|
|
367
|
+
completed.add(name);
|
|
368
|
+
remaining.delete(name);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return stages;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Detect cycles in the dependency graph.
|
|
375
|
+
*/
|
|
376
|
+
detectCycle() {
|
|
377
|
+
const visited = new Set();
|
|
378
|
+
const recStack = new Set();
|
|
379
|
+
const path = [];
|
|
380
|
+
const dfs = (name) => {
|
|
381
|
+
visited.add(name);
|
|
382
|
+
recStack.add(name);
|
|
383
|
+
path.push(name);
|
|
384
|
+
const model = this.models.get(name);
|
|
385
|
+
if (model) {
|
|
386
|
+
const upstream = model.getUpstreamModel();
|
|
387
|
+
if (upstream && this.models.has(upstream.name)) {
|
|
388
|
+
if (!visited.has(upstream.name)) {
|
|
389
|
+
const cycle = dfs(upstream.name);
|
|
390
|
+
if (cycle)
|
|
391
|
+
return cycle;
|
|
392
|
+
}
|
|
393
|
+
else if (recStack.has(upstream.name)) {
|
|
394
|
+
// Found cycle
|
|
395
|
+
const cycleStart = path.indexOf(upstream.name);
|
|
396
|
+
return [...path.slice(cycleStart), upstream.name];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
path.pop();
|
|
401
|
+
recStack.delete(name);
|
|
402
|
+
return null;
|
|
403
|
+
};
|
|
404
|
+
for (const name of this.models.keys()) {
|
|
405
|
+
if (!visited.has(name)) {
|
|
406
|
+
const cycle = dfs(name);
|
|
407
|
+
if (cycle)
|
|
408
|
+
return cycle;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Render dependency graph as Mermaid.
|
|
415
|
+
*/
|
|
416
|
+
toMermaidFromDeps(deps) {
|
|
417
|
+
const lines = ["graph TD"];
|
|
418
|
+
// Define all nodes with labels first
|
|
419
|
+
for (const [name] of deps) {
|
|
420
|
+
const model = this.models.get(name);
|
|
421
|
+
const matType = model?.materialize.type ?? "unknown";
|
|
422
|
+
lines.push(` ${name}["${name}<br/>(${matType})"]`);
|
|
423
|
+
}
|
|
424
|
+
// Then add edges
|
|
425
|
+
for (const [name, modelDeps] of deps) {
|
|
426
|
+
for (const dep of modelDeps) {
|
|
427
|
+
lines.push(` ${dep} --> ${name}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return lines.join("\n");
|
|
431
|
+
}
|
|
432
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tmql-orchestration",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "DAG composition and orchestration layer for tmql - Build data pipelines with TMModel and TMProject",
|
|
5
|
+
"author": "Ryan Peggs & Tim Vyas",
|
|
6
|
+
"license": "Elastic-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/covahltd/tmql.git",
|
|
10
|
+
"directory": "packages/tmql-orchestration"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mongodb",
|
|
14
|
+
"dag",
|
|
15
|
+
"orchestration",
|
|
16
|
+
"data-pipeline",
|
|
17
|
+
"etl",
|
|
18
|
+
"tmql",
|
|
19
|
+
"materialization"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"build:watch": "tsc --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"tmql": ">=0.4.0 <1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"mongodb": "^6.17.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tmql": "workspace:*",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|