@malloy-publisher/server 0.0.119 → 0.0.120
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/app/api-doc.yaml +324 -335
- package/dist/app/assets/{HomePage-BxFnfH3M.js → HomePage-xhvGPTSO.js} +1 -1
- package/dist/app/assets/{MainPage-D301Y0mT.js → MainPage-Bq95p8Cl.js} +1 -1
- package/dist/app/assets/{ModelPage-Df8ivC1J.js → ModelPage-CbfBNWIi.js} +1 -1
- package/dist/app/assets/{PackagePage-CE41SCV_.js → PackagePage-CGS612C4.js} +1 -1
- package/dist/app/assets/ProjectPage-Dpn9pqSB.js +1 -0
- package/dist/app/assets/{RouteError-l_WGtNhS.js → RouteError-BLPhl1wC.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CY-1oBvt.js → WorkbookPage-Dt93gSZ3.js} +1 -1
- package/dist/app/assets/{index-D5BBaLz8.js → index-B8wuAjgG.js} +1 -1
- package/dist/app/assets/{index-DjbXd602.js → index-CVbROKL7.js} +113 -113
- package/dist/app/assets/{index-DlZbNvNc.js → index-DxKW6bXB.js} +1 -1
- package/dist/app/assets/{index.umd-DQiSWsWe.js → index.umd-CVy5LWk2.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.js +35396 -144724
- package/k6-tests/common.ts +12 -3
- package/package.json +1 -1
- package/src/controller/connection.controller.ts +82 -72
- package/src/controller/query.controller.ts +1 -1
- package/src/server.ts +6 -48
- package/src/service/connection.ts +384 -305
- package/src/service/db_utils.ts +407 -303
- package/src/service/package.spec.ts +8 -97
- package/src/service/package.ts +24 -46
- package/src/service/project.ts +8 -24
- package/src/service/project_store.ts +0 -1
- package/dist/app/assets/ProjectPage-DA66xbmQ.js +0 -1
- package/src/controller/schedule.controller.ts +0 -21
- package/src/service/scheduler.ts +0 -190
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { Stats } from "fs";
|
|
2
3
|
import fs from "fs/promises";
|
|
3
4
|
import { join } from "path";
|
|
4
5
|
import sinon from "sinon";
|
|
5
6
|
import { PackageNotFoundError } from "../errors";
|
|
6
|
-
import { readConnectionConfig } from "./connection";
|
|
7
7
|
import { Model } from "./model";
|
|
8
8
|
import { Package } from "./package";
|
|
9
|
-
import { Scheduler } from "./scheduler";
|
|
10
|
-
import { Stats } from "fs";
|
|
11
9
|
|
|
12
10
|
// Minimal partial types for mocking
|
|
13
|
-
type
|
|
11
|
+
type PartialModel = Pick<Model, "getPath">;
|
|
14
12
|
|
|
15
13
|
describe("service/package", () => {
|
|
16
14
|
const testPackageDirectory = "testPackage";
|
|
@@ -48,16 +46,13 @@ describe("service/package", () => {
|
|
|
48
46
|
new Map([
|
|
49
47
|
[
|
|
50
48
|
"model1.malloy",
|
|
51
|
-
|
|
52
|
-
{ getPath: () => "model1.malloy" } as PartialModel,
|
|
49
|
+
{ getPath: () => "model1.malloy" } as unknown as Model,
|
|
53
50
|
],
|
|
54
51
|
[
|
|
55
52
|
"model2.malloynb",
|
|
56
|
-
|
|
57
|
-
{ getPath: () => "model2.malloynb" } as PartialModel,
|
|
53
|
+
{ getPath: () => "model2.malloynb" } as unknown as Model,
|
|
58
54
|
],
|
|
59
55
|
]),
|
|
60
|
-
undefined,
|
|
61
56
|
);
|
|
62
57
|
|
|
63
58
|
expect(pkg).toBeInstanceOf(Package);
|
|
@@ -75,7 +70,7 @@ describe("service/package", () => {
|
|
|
75
70
|
"testPackage",
|
|
76
71
|
testPackageDirectory,
|
|
77
72
|
new Map(),
|
|
78
|
-
|
|
73
|
+
[],
|
|
79
74
|
),
|
|
80
75
|
).rejects.toThrowError(
|
|
81
76
|
new PackageNotFoundError(
|
|
@@ -96,17 +91,11 @@ describe("service/package", () => {
|
|
|
96
91
|
);
|
|
97
92
|
|
|
98
93
|
// Still use Partial<Model> for the stub resolution type
|
|
99
|
-
type PartialModel = Pick<Model, "getPath">;
|
|
100
94
|
sinon
|
|
101
95
|
.stub(Model, "create")
|
|
102
96
|
// @ts-expect-error PartialModel is a partial type
|
|
103
97
|
.resolves({ getPath: () => "model1.model" } as PartialModel);
|
|
104
98
|
|
|
105
|
-
// @ts-expect-error PartialScheduler is a partial type
|
|
106
|
-
sinon.stub(Scheduler, "create").returns({
|
|
107
|
-
list: () => [],
|
|
108
|
-
} as PartialScheduler);
|
|
109
|
-
|
|
110
99
|
readFileStub.restore();
|
|
111
100
|
readFileStub.resolves(Buffer.from(JSON.stringify([])));
|
|
112
101
|
|
|
@@ -115,7 +104,7 @@ describe("service/package", () => {
|
|
|
115
104
|
"testPackage",
|
|
116
105
|
testPackageDirectory,
|
|
117
106
|
new Map(),
|
|
118
|
-
|
|
107
|
+
[],
|
|
119
108
|
);
|
|
120
109
|
|
|
121
110
|
expect(packageInstance).toBeInstanceOf(Package);
|
|
@@ -138,7 +127,6 @@ describe("service/package", () => {
|
|
|
138
127
|
},
|
|
139
128
|
]);
|
|
140
129
|
expect(packageInstance.listModels()).toBeEmpty();
|
|
141
|
-
expect(packageInstance.listSchedules()).toBeEmpty();
|
|
142
130
|
},
|
|
143
131
|
{ timeout: 15000 },
|
|
144
132
|
);
|
|
@@ -159,8 +147,7 @@ describe("service/package", () => {
|
|
|
159
147
|
{
|
|
160
148
|
getPath: () => "model1.malloy",
|
|
161
149
|
getModel: () => "foo",
|
|
162
|
-
|
|
163
|
-
} as PartialModel,
|
|
150
|
+
} as unknown as Model,
|
|
164
151
|
],
|
|
165
152
|
[
|
|
166
153
|
"model2.malloynb",
|
|
@@ -171,11 +158,9 @@ describe("service/package", () => {
|
|
|
171
158
|
message: "This is the error",
|
|
172
159
|
};
|
|
173
160
|
},
|
|
174
|
-
|
|
175
|
-
} as PartialModel,
|
|
161
|
+
} as unknown as Model,
|
|
176
162
|
],
|
|
177
163
|
]),
|
|
178
|
-
undefined,
|
|
179
164
|
);
|
|
180
165
|
|
|
181
166
|
const models = await packageInstance.listModels();
|
|
@@ -222,79 +207,5 @@ describe("service/package", () => {
|
|
|
222
207
|
});
|
|
223
208
|
});
|
|
224
209
|
});
|
|
225
|
-
|
|
226
|
-
describe("readConnectionConfig", () => {
|
|
227
|
-
it("should return an empty array if no project name or server root path is provided", async () => {
|
|
228
|
-
const config = await readConnectionConfig(testPackageDirectory);
|
|
229
|
-
expect(Array.isArray(config)).toBe(true);
|
|
230
|
-
expect(config).toHaveLength(0);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("should return an empty array if project name or server root path is missing", async () => {
|
|
234
|
-
const config1 = await readConnectionConfig(
|
|
235
|
-
testPackageDirectory,
|
|
236
|
-
"testProject",
|
|
237
|
-
);
|
|
238
|
-
expect(Array.isArray(config1)).toBe(true);
|
|
239
|
-
expect(config1).toHaveLength(0);
|
|
240
|
-
|
|
241
|
-
const config2 = await readConnectionConfig(
|
|
242
|
-
testPackageDirectory,
|
|
243
|
-
undefined,
|
|
244
|
-
"/server/root",
|
|
245
|
-
);
|
|
246
|
-
expect(Array.isArray(config2)).toBe(true);
|
|
247
|
-
expect(config2).toHaveLength(0);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("should return connections from publisher config when project name and server root are provided", async () => {
|
|
251
|
-
const tempServerRoot = "/tmp/test-server-root";
|
|
252
|
-
const tempConfigPath = join(
|
|
253
|
-
tempServerRoot,
|
|
254
|
-
"publisher.config.json",
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Create temp directory and config file
|
|
258
|
-
await fs.mkdir(tempServerRoot, { recursive: true });
|
|
259
|
-
await fs.writeFile(
|
|
260
|
-
tempConfigPath,
|
|
261
|
-
JSON.stringify({
|
|
262
|
-
frozenConfig: false,
|
|
263
|
-
projects: [
|
|
264
|
-
{
|
|
265
|
-
name: "testProject",
|
|
266
|
-
packages: [],
|
|
267
|
-
connections: [
|
|
268
|
-
{ name: "test-conn", type: "postgres" },
|
|
269
|
-
{ name: "test-conn2", type: "bigquery" },
|
|
270
|
-
],
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
}),
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
const config = await readConnectionConfig(
|
|
277
|
-
testPackageDirectory,
|
|
278
|
-
"testProject",
|
|
279
|
-
tempServerRoot,
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
expect(Array.isArray(config)).toBe(true);
|
|
283
|
-
expect(config).toHaveLength(2);
|
|
284
|
-
expect(config[0]).toMatchObject({
|
|
285
|
-
name: "test-conn",
|
|
286
|
-
type: "postgres",
|
|
287
|
-
resource: "/api/v0/connections/test-conn",
|
|
288
|
-
});
|
|
289
|
-
expect(config[1]).toMatchObject({
|
|
290
|
-
name: "test-conn2",
|
|
291
|
-
type: "bigquery",
|
|
292
|
-
resource: "/api/v0/connections/test-conn2",
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Clean up
|
|
296
|
-
await fs.rm(tempServerRoot, { recursive: true, force: true });
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
210
|
});
|
|
300
211
|
});
|
package/src/service/package.ts
CHANGED
|
@@ -19,15 +19,13 @@ import {
|
|
|
19
19
|
} from "../constants";
|
|
20
20
|
import { PackageNotFoundError } from "../errors";
|
|
21
21
|
import { logger } from "../logger";
|
|
22
|
-
import {
|
|
23
|
-
import { Model } from "./model";
|
|
24
|
-
import { Scheduler } from "./scheduler";
|
|
22
|
+
import { createPackageDuckDBConnections } from "./connection";
|
|
23
|
+
import { ApiConnection, Model } from "./model";
|
|
25
24
|
|
|
26
25
|
type ApiDatabase = components["schemas"]["Database"];
|
|
27
26
|
type ApiModel = components["schemas"]["Model"];
|
|
28
27
|
type ApiNotebook = components["schemas"]["Notebook"];
|
|
29
28
|
export type ApiPackage = components["schemas"]["Package"];
|
|
30
|
-
type ApiSchedule = components["schemas"]["Schedule"];
|
|
31
29
|
type ApiColumn = components["schemas"]["Column"];
|
|
32
30
|
type ApiTableDescription = components["schemas"]["TableDescription"];
|
|
33
31
|
|
|
@@ -38,8 +36,8 @@ export class Package {
|
|
|
38
36
|
private packageMetadata: ApiPackage;
|
|
39
37
|
private databases: ApiDatabase[];
|
|
40
38
|
private models: Map<string, Model> = new Map();
|
|
41
|
-
private scheduler: Scheduler | undefined;
|
|
42
39
|
private packagePath: string;
|
|
40
|
+
private connections: Map<string, Connection> = new Map();
|
|
43
41
|
private static meter = metrics.getMeter("publisher");
|
|
44
42
|
private static packageLoadHistogram = this.meter.createHistogram(
|
|
45
43
|
"malloy_package_load_duration",
|
|
@@ -56,7 +54,7 @@ export class Package {
|
|
|
56
54
|
packageMetadata: ApiPackage,
|
|
57
55
|
databases: ApiDatabase[],
|
|
58
56
|
models: Map<string, Model>,
|
|
59
|
-
|
|
57
|
+
connections: Map<string, Connection> = new Map(),
|
|
60
58
|
) {
|
|
61
59
|
this.projectName = projectName;
|
|
62
60
|
this.packageName = packageName;
|
|
@@ -64,7 +62,7 @@ export class Package {
|
|
|
64
62
|
this.packageMetadata = packageMetadata;
|
|
65
63
|
this.databases = databases;
|
|
66
64
|
this.models = models;
|
|
67
|
-
this.
|
|
65
|
+
this.connections = connections;
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
static async create(
|
|
@@ -72,7 +70,7 @@ export class Package {
|
|
|
72
70
|
packageName: string,
|
|
73
71
|
packagePath: string,
|
|
74
72
|
projectConnections: Map<string, Connection>,
|
|
75
|
-
|
|
73
|
+
packageConnections: ApiConnection[],
|
|
76
74
|
): Promise<Package> {
|
|
77
75
|
const startTime = performance.now();
|
|
78
76
|
await Package.validatePackageManifestExistsOrThrowError(packagePath);
|
|
@@ -102,34 +100,15 @@ export class Package {
|
|
|
102
100
|
unit: "ms",
|
|
103
101
|
});
|
|
104
102
|
const connections = new Map<string, Connection>(projectConnections);
|
|
105
|
-
logger.info(`Project connections: ${connections.size}`, {
|
|
106
|
-
connections,
|
|
107
|
-
projectConnections,
|
|
108
|
-
});
|
|
109
|
-
// Package connections override project connections.
|
|
110
|
-
const { malloyConnections: packageConnections } =
|
|
111
|
-
await createConnections(
|
|
112
|
-
packagePath,
|
|
113
|
-
[],
|
|
114
|
-
projectName,
|
|
115
|
-
serverRootPath,
|
|
116
|
-
);
|
|
117
|
-
const connectionsTime = performance.now();
|
|
118
|
-
logger.info("Package connections created", {
|
|
119
|
-
packageName,
|
|
120
|
-
connectionCount: packageConnections.size,
|
|
121
|
-
duration: connectionsTime - databasesTime,
|
|
122
|
-
unit: "ms",
|
|
123
|
-
});
|
|
124
|
-
packageConnections.forEach((connection) => {
|
|
125
|
-
connections.set(connection.name, connection);
|
|
126
|
-
});
|
|
127
103
|
|
|
128
104
|
// Add a duckdb connection for the package.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
const duckdbConnections = await createPackageDuckDBConnections(
|
|
106
|
+
packageConnections,
|
|
107
|
+
packagePath,
|
|
132
108
|
);
|
|
109
|
+
duckdbConnections.malloyConnections.forEach((connection, name) => {
|
|
110
|
+
connections.set(name, connection);
|
|
111
|
+
});
|
|
133
112
|
|
|
134
113
|
const models = await Package.loadModels(
|
|
135
114
|
packageName,
|
|
@@ -140,14 +119,7 @@ export class Package {
|
|
|
140
119
|
logger.info("Models loaded", {
|
|
141
120
|
packageName,
|
|
142
121
|
modelCount: models.size,
|
|
143
|
-
duration: modelsTime -
|
|
144
|
-
unit: "ms",
|
|
145
|
-
});
|
|
146
|
-
const scheduler = Scheduler.create(models);
|
|
147
|
-
const schedulerTime = performance.now();
|
|
148
|
-
logger.info("Scheduler created", {
|
|
149
|
-
packageName,
|
|
150
|
-
duration: schedulerTime - modelsTime,
|
|
122
|
+
duration: modelsTime - databasesTime,
|
|
151
123
|
unit: "ms",
|
|
152
124
|
});
|
|
153
125
|
const endTime = performance.now();
|
|
@@ -168,7 +140,7 @@ export class Package {
|
|
|
168
140
|
packageConfig,
|
|
169
141
|
databases,
|
|
170
142
|
models,
|
|
171
|
-
|
|
143
|
+
connections,
|
|
172
144
|
);
|
|
173
145
|
} catch (error) {
|
|
174
146
|
logger.error(`Error loading package ${packageName}`, { error });
|
|
@@ -195,14 +167,20 @@ export class Package {
|
|
|
195
167
|
return this.databases;
|
|
196
168
|
}
|
|
197
169
|
|
|
198
|
-
public listSchedules(): ApiSchedule[] {
|
|
199
|
-
return this.scheduler ? this.scheduler.list() : [];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
170
|
public getModel(modelPath: string): Model | undefined {
|
|
203
171
|
return this.models.get(modelPath);
|
|
204
172
|
}
|
|
205
173
|
|
|
174
|
+
public getMalloyConnection(connectionName: string): Connection {
|
|
175
|
+
const connection = this.connections.get(connectionName);
|
|
176
|
+
if (!connection) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Connection ${connectionName} not found in package ${this.packageName}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return connection;
|
|
182
|
+
}
|
|
183
|
+
|
|
206
184
|
public async getModelFileText(modelPath: string): Promise<string> {
|
|
207
185
|
const model = this.getModel(modelPath);
|
|
208
186
|
if (!model) {
|
package/src/service/project.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
ProjectNotFoundError,
|
|
11
11
|
} from "../errors";
|
|
12
12
|
import { logger } from "../logger";
|
|
13
|
-
import {
|
|
13
|
+
import { createProjectConnections, InternalConnection } from "./connection";
|
|
14
14
|
import { ApiConnection } from "./model";
|
|
15
15
|
import { Package } from "./package";
|
|
16
16
|
|
|
@@ -37,7 +37,6 @@ export class Project {
|
|
|
37
37
|
private apiConnections: ApiConnection[];
|
|
38
38
|
private projectPath: string;
|
|
39
39
|
private projectName: string;
|
|
40
|
-
private serverRootPath: string;
|
|
41
40
|
public metadata: ApiProject;
|
|
42
41
|
|
|
43
42
|
constructor(
|
|
@@ -45,11 +44,9 @@ export class Project {
|
|
|
45
44
|
projectPath: string,
|
|
46
45
|
malloyConnections: Map<string, BaseConnection>,
|
|
47
46
|
apiConnections: InternalConnection[],
|
|
48
|
-
serverRootPath: string,
|
|
49
47
|
) {
|
|
50
48
|
this.projectName = projectName;
|
|
51
49
|
this.projectPath = projectPath;
|
|
52
|
-
this.serverRootPath = serverRootPath;
|
|
53
50
|
this.malloyConnections = malloyConnections;
|
|
54
51
|
this.apiConnections = apiConnections;
|
|
55
52
|
this.metadata = {
|
|
@@ -73,10 +70,8 @@ export class Project {
|
|
|
73
70
|
);
|
|
74
71
|
|
|
75
72
|
// Reload connections with full config
|
|
76
|
-
const { malloyConnections, apiConnections } =
|
|
77
|
-
|
|
78
|
-
payload.connections,
|
|
79
|
-
);
|
|
73
|
+
const { malloyConnections, apiConnections } =
|
|
74
|
+
await createProjectConnections(payload.connections);
|
|
80
75
|
|
|
81
76
|
// Update the project's connection maps
|
|
82
77
|
this.malloyConnections = malloyConnections;
|
|
@@ -98,8 +93,7 @@ export class Project {
|
|
|
98
93
|
static async create(
|
|
99
94
|
projectName: string,
|
|
100
95
|
projectPath: string,
|
|
101
|
-
|
|
102
|
-
serverRootPath?: string,
|
|
96
|
+
connections: ApiConnection[],
|
|
103
97
|
): Promise<Project> {
|
|
104
98
|
if (!(await fs.promises.stat(projectPath)).isDirectory()) {
|
|
105
99
|
throw new ProjectNotFoundError(
|
|
@@ -107,18 +101,9 @@ export class Project {
|
|
|
107
101
|
);
|
|
108
102
|
}
|
|
109
103
|
|
|
110
|
-
let malloyConnections: Map<string, BaseConnection> = new Map();
|
|
111
|
-
let apiConnections: InternalConnection[] = [];
|
|
112
|
-
|
|
113
104
|
logger.info(`Creating project with connection configuration`);
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
defaultConnections,
|
|
117
|
-
projectName,
|
|
118
|
-
serverRootPath,
|
|
119
|
-
);
|
|
120
|
-
malloyConnections = result.malloyConnections;
|
|
121
|
-
apiConnections = result.apiConnections;
|
|
105
|
+
const { malloyConnections, apiConnections } =
|
|
106
|
+
await createProjectConnections(connections);
|
|
122
107
|
|
|
123
108
|
logger.info(
|
|
124
109
|
`Loaded ${malloyConnections.size + apiConnections.length} connections for project ${projectName}`,
|
|
@@ -133,7 +118,6 @@ export class Project {
|
|
|
133
118
|
projectPath,
|
|
134
119
|
malloyConnections,
|
|
135
120
|
apiConnections,
|
|
136
|
-
serverRootPath || "",
|
|
137
121
|
);
|
|
138
122
|
}
|
|
139
123
|
|
|
@@ -264,7 +248,7 @@ export class Project {
|
|
|
264
248
|
packageName,
|
|
265
249
|
path.join(this.projectPath, packageName),
|
|
266
250
|
this.malloyConnections,
|
|
267
|
-
this.
|
|
251
|
+
this.apiConnections,
|
|
268
252
|
);
|
|
269
253
|
this.packages.set(packageName, _package);
|
|
270
254
|
|
|
@@ -309,7 +293,7 @@ export class Project {
|
|
|
309
293
|
packageName,
|
|
310
294
|
packagePath,
|
|
311
295
|
this.malloyConnections,
|
|
312
|
-
this.
|
|
296
|
+
this.apiConnections,
|
|
313
297
|
),
|
|
314
298
|
);
|
|
315
299
|
} catch (error) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{G as t,p as n,j as e,F as o,N as c}from"./index-DjbXd602.js";function j(){const r=t(),{projectName:s}=n();if(s){const a=o({projectName:s});return e.jsx(c,{onSelectPackage:r,resourceUri:a})}else return e.jsx("div",{children:e.jsx("h2",{children:"Missing project name"})})}export{j as default};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { components } from "../api";
|
|
2
|
-
import { ProjectStore } from "../service/project_store";
|
|
3
|
-
|
|
4
|
-
type ApiSchedule = components["schemas"]["Schedule"];
|
|
5
|
-
|
|
6
|
-
export class ScheduleController {
|
|
7
|
-
private projectStore: ProjectStore;
|
|
8
|
-
|
|
9
|
-
constructor(projectStore: ProjectStore) {
|
|
10
|
-
this.projectStore = projectStore;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public async listSchedules(
|
|
14
|
-
projectName: string,
|
|
15
|
-
packageName: string,
|
|
16
|
-
): Promise<ApiSchedule[]> {
|
|
17
|
-
const project = await this.projectStore?.getProject?.(projectName);
|
|
18
|
-
const p = await project?.getPackage?.(packageName);
|
|
19
|
-
return p?.listSchedules?.();
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/service/scheduler.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import cron from "node-cron";
|
|
2
|
-
import { components } from "../api";
|
|
3
|
-
import { logger } from "../logger";
|
|
4
|
-
import { Model } from "./model";
|
|
5
|
-
|
|
6
|
-
// @ts-expect-error TODO: Fix missing Source type in API
|
|
7
|
-
type ApiSource = components["schemas"]["Source"];
|
|
8
|
-
type ApiView = components["schemas"]["View"];
|
|
9
|
-
type ApiQuery = components["schemas"]["Query"];
|
|
10
|
-
type ApiSchedule = components["schemas"]["Schedule"];
|
|
11
|
-
|
|
12
|
-
const SCHEDULE_ANNOTATION = "# schedule";
|
|
13
|
-
|
|
14
|
-
class Schedule {
|
|
15
|
-
private model: Model;
|
|
16
|
-
private source: ApiSource | undefined;
|
|
17
|
-
private view: ApiView | undefined;
|
|
18
|
-
private query: ApiQuery | undefined;
|
|
19
|
-
|
|
20
|
-
private schedule: string;
|
|
21
|
-
private action: string;
|
|
22
|
-
private connection: string;
|
|
23
|
-
private task: cron.ScheduledTask;
|
|
24
|
-
private lastRunTime: number | undefined;
|
|
25
|
-
private lastRunStatus: string;
|
|
26
|
-
|
|
27
|
-
public constructor(
|
|
28
|
-
model: Model,
|
|
29
|
-
source: ApiSource | undefined,
|
|
30
|
-
view: ApiView | undefined,
|
|
31
|
-
query: ApiQuery | undefined,
|
|
32
|
-
annotation: string,
|
|
33
|
-
) {
|
|
34
|
-
this.model = model;
|
|
35
|
-
this.source = source;
|
|
36
|
-
this.view = view;
|
|
37
|
-
this.query = query;
|
|
38
|
-
const { origSchedule, cronSchedule, action, connection } =
|
|
39
|
-
Schedule.parseAnnotation(annotation);
|
|
40
|
-
this.schedule = origSchedule;
|
|
41
|
-
this.action = action;
|
|
42
|
-
this.connection = connection;
|
|
43
|
-
this.lastRunTime = undefined;
|
|
44
|
-
this.lastRunStatus = "unknown";
|
|
45
|
-
|
|
46
|
-
this.task = cron.schedule(cronSchedule, () => {
|
|
47
|
-
this.lastRunTime = Date.now();
|
|
48
|
-
this.lastRunStatus = "ok";
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public stop() {
|
|
53
|
-
this.task.stop();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public get(): ApiSchedule {
|
|
57
|
-
const query = this.source
|
|
58
|
-
? `${this.source.name} > ${this.view?.name}`
|
|
59
|
-
: this.query?.name;
|
|
60
|
-
return {
|
|
61
|
-
resource: `${this.model.getPath()} > ${query}`,
|
|
62
|
-
schedule: this.schedule,
|
|
63
|
-
action: this.action,
|
|
64
|
-
connection: this.connection,
|
|
65
|
-
lastRunTime: this.lastRunTime,
|
|
66
|
-
lastRunStatus: this.lastRunStatus,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private static parseAnnotation(annotation: string) {
|
|
71
|
-
// Example schedule annotation
|
|
72
|
-
// # schedule @hourly materialize duckdb
|
|
73
|
-
// # schedule "0 * * * *" materialize duckdb
|
|
74
|
-
// TODO: Don't split quoted strings. Use regex instead of split.
|
|
75
|
-
const annotationSplit = annotation.split(/\s+/);
|
|
76
|
-
if (annotationSplit.length != 6) {
|
|
77
|
-
logger.info("Length: " + annotationSplit.length);
|
|
78
|
-
throw new Error(
|
|
79
|
-
"Invalid annotation string does not have enough parts: " +
|
|
80
|
-
annotation,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (annotationSplit[0] != "#") {
|
|
85
|
-
throw new Error(
|
|
86
|
-
"Invalid annotation string does not have start with #: " +
|
|
87
|
-
annotationSplit[0],
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (annotationSplit[1] != "schedule") {
|
|
92
|
-
throw new Error(
|
|
93
|
-
"Invalid annotation string does not start with schedule command: " +
|
|
94
|
-
annotationSplit[1],
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const standardCron = Schedule.translateNonStandardCron(
|
|
99
|
-
annotationSplit[2],
|
|
100
|
-
);
|
|
101
|
-
if (!cron.validate(standardCron)) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
"Invalid annotation string does not have valid cron schedule: " +
|
|
104
|
-
standardCron,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
annotationSplit[3] != "materialize" &&
|
|
110
|
-
annotationSplit[3] != "report"
|
|
111
|
-
) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
"Invalid annotation string unrecognized command: " + annotation,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// TODO: Validate connection exists.
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
origSchedule: annotationSplit[2],
|
|
121
|
-
cronSchedule: standardCron,
|
|
122
|
-
action: annotationSplit[3],
|
|
123
|
-
connection: annotationSplit[4],
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
public static translateNonStandardCron(schedule: string): string {
|
|
128
|
-
let standardCron = schedule;
|
|
129
|
-
switch (schedule) {
|
|
130
|
-
case "@yearly":
|
|
131
|
-
case "@anually":
|
|
132
|
-
standardCron = "0 0 1 1 *";
|
|
133
|
-
break;
|
|
134
|
-
case "@monthly":
|
|
135
|
-
standardCron = "0 0 1 * *";
|
|
136
|
-
break;
|
|
137
|
-
case "@weekly":
|
|
138
|
-
standardCron = "0 0 * * 0";
|
|
139
|
-
break;
|
|
140
|
-
case "@daily":
|
|
141
|
-
case "@midnight":
|
|
142
|
-
standardCron = "0 0 * * *";
|
|
143
|
-
break;
|
|
144
|
-
case "@hourly":
|
|
145
|
-
standardCron = "0 * * * *";
|
|
146
|
-
break;
|
|
147
|
-
case "@minutely":
|
|
148
|
-
standardCron = "* * * * *";
|
|
149
|
-
}
|
|
150
|
-
return standardCron;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export class Scheduler {
|
|
155
|
-
private schedules: Schedule[];
|
|
156
|
-
|
|
157
|
-
private constructor(schedules: Schedule[]) {
|
|
158
|
-
this.schedules = schedules;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
public static create(models: Map<string, Model>): Scheduler {
|
|
162
|
-
const schedules: Schedule[] = [];
|
|
163
|
-
|
|
164
|
-
models.forEach((m) => {
|
|
165
|
-
m.getSources()?.forEach((s) => {
|
|
166
|
-
s.views?.forEach((v: ApiView) => {
|
|
167
|
-
v.annotations?.forEach((a: string) => {
|
|
168
|
-
if (a.startsWith(SCHEDULE_ANNOTATION)) {
|
|
169
|
-
schedules.push(new Schedule(m, s, v, undefined, a));
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
m.getQueries()?.forEach((q) => {
|
|
176
|
-
q.annotations?.forEach((a) => {
|
|
177
|
-
if (a.startsWith(SCHEDULE_ANNOTATION)) {
|
|
178
|
-
schedules.push(new Schedule(m, undefined, undefined, q, a));
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
return new Scheduler(schedules);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
public list(): ApiSchedule[] {
|
|
188
|
-
return this.schedules.map((s) => s.get());
|
|
189
|
-
}
|
|
190
|
-
}
|