@malloy-publisher/server 0.0.382-dev → 0.2.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.
Files changed (175) hide show
  1. package/README.docker.md +145 -20
  2. package/README.md +65 -12
  3. package/dist/app/api-doc.html +5 -0
  4. package/dist/app/api-doc.yaml +3903 -583
  5. package/dist/app/assets/EnvironmentPage-DiUgLuCH.js +1 -0
  6. package/dist/app/assets/HomePage-MhhjhnFj.js +1 -0
  7. package/dist/app/assets/LightMode-BEyvNUzT.js +1 -0
  8. package/dist/app/assets/MainPage-D4Cfee9o.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-nGgGqwSN.js +1 -0
  10. package/dist/app/assets/ModelPage-DLAcEw-F.js +1 -0
  11. package/dist/app/assets/PackagePage-Df0ikH7w.js +1 -0
  12. package/dist/app/assets/RouteError-BFQwtroH.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-OoskoQYN.js +1 -0
  14. package/dist/app/assets/WorkbookPage-CpFgvUDC.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-lb28vQkR.es-BoVSOc75.js} +1 -1
  16. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  17. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  18. package/dist/app/assets/index-6ypuFH6g.js +1774 -0
  19. package/dist/app/assets/index-B8pnU_V_.js +615 -0
  20. package/dist/app/assets/index-BImcHmVP.js +1 -0
  21. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  22. package/dist/app/assets/index-DfqzSItA.js +2 -0
  23. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  24. package/dist/app/index.html +8 -4
  25. package/dist/cpufeatures-1yrn0vtw.node +0 -0
  26. package/dist/default-publisher.config.json +23 -0
  27. package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
  28. package/dist/package_load_worker.mjs +15219 -0
  29. package/dist/runtime/publisher.js +331 -0
  30. package/dist/{server.js → server.mjs} +115548 -49590
  31. package/dist/sshcrypto-vd2k5hq9.node +0 -0
  32. package/package.json +41 -28
  33. package/publisher.config.example.bigquery.json +33 -0
  34. package/publisher.config.example.duckdb.json +23 -0
  35. package/.eslintrc.json +0 -44
  36. package/.prettierignore +0 -1
  37. package/.prettierrc +0 -4
  38. package/build.ts +0 -38
  39. package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
  40. package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
  41. package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
  42. package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
  43. package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
  44. package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
  45. package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
  46. package/dist/app/assets/index-15BOvhp0.js +0 -456
  47. package/dist/app/assets/index-Bb2jqquW.js +0 -676
  48. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  49. package/dist/app/assets/index-D68X76-7.js +0 -1276
  50. package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
  51. package/dxt/malloy_bridge.py +0 -354
  52. package/dxt/manifest.json +0 -22
  53. package/eslint.config.mjs +0 -76
  54. package/k6-tests/README.md +0 -144
  55. package/k6-tests/bun.lock +0 -797
  56. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  57. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  58. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  59. package/k6-tests/load-test/load-test-crud.ts +0 -142
  60. package/k6-tests/load-test/load-test.ts +0 -784
  61. package/k6-tests/package.json +0 -27
  62. package/k6-tests/scripts/run-with-otel.sh +0 -50
  63. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  64. package/k6-tests/tsconfig.json +0 -29
  65. package/k6-tests/utils/client_factory.ts +0 -128
  66. package/k6-tests/utils/common.ts +0 -489
  67. package/k6-tests/utils/logger.ts +0 -62
  68. package/k6-tests/utils/types.d.ts +0 -35
  69. package/malloy_mcp.dxt +0 -0
  70. package/publisher.config.json +0 -33
  71. package/src/config.spec.ts +0 -850
  72. package/src/config.ts +0 -305
  73. package/src/constants.ts +0 -10
  74. package/src/controller/compile.controller.ts +0 -35
  75. package/src/controller/connection.controller.ts +0 -515
  76. package/src/controller/database.controller.ts +0 -21
  77. package/src/controller/manifest.controller.ts +0 -29
  78. package/src/controller/materialization.controller.ts +0 -125
  79. package/src/controller/model.controller.ts +0 -107
  80. package/src/controller/package.controller.ts +0 -182
  81. package/src/controller/query.controller.ts +0 -60
  82. package/src/controller/watch-mode.controller.ts +0 -98
  83. package/src/data_styles.ts +0 -71
  84. package/src/dto/connection.dto.spec.ts +0 -131
  85. package/src/dto/connection.dto.ts +0 -174
  86. package/src/dto/index.ts +0 -2
  87. package/src/dto/package.dto.spec.ts +0 -42
  88. package/src/dto/package.dto.ts +0 -12
  89. package/src/dto/validate.spec.ts +0 -76
  90. package/src/dto/validate.ts +0 -31
  91. package/src/errors.ts +0 -116
  92. package/src/health.ts +0 -189
  93. package/src/instrumentation.ts +0 -148
  94. package/src/logger.ts +0 -154
  95. package/src/mcp/error_messages.ts +0 -199
  96. package/src/mcp/handler_utils.ts +0 -239
  97. package/src/mcp/mcp_constants.ts +0 -22
  98. package/src/mcp/prompts/handlers.ts +0 -84
  99. package/src/mcp/prompts/index.ts +0 -11
  100. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  101. package/src/mcp/prompts/prompt_service.ts +0 -67
  102. package/src/mcp/prompts/utils.ts +0 -62
  103. package/src/mcp/resource_metadata.ts +0 -47
  104. package/src/mcp/resources/model_resource.ts +0 -153
  105. package/src/mcp/resources/notebook_resource.ts +0 -137
  106. package/src/mcp/resources/package_resource.ts +0 -370
  107. package/src/mcp/resources/project_resource.ts +0 -184
  108. package/src/mcp/resources/query_resource.ts +0 -117
  109. package/src/mcp/resources/source_resource.ts +0 -141
  110. package/src/mcp/resources/view_resource.ts +0 -136
  111. package/src/mcp/server.ts +0 -62
  112. package/src/mcp/tools/discovery_tools.ts +0 -258
  113. package/src/mcp/tools/execute_query_tool.ts +0 -265
  114. package/src/server.ts +0 -1195
  115. package/src/service/connection.spec.ts +0 -1331
  116. package/src/service/connection.ts +0 -1511
  117. package/src/service/connection_service.spec.ts +0 -641
  118. package/src/service/connection_service.ts +0 -181
  119. package/src/service/db_utils.spec.ts +0 -714
  120. package/src/service/db_utils.ts +0 -1227
  121. package/src/service/filter.spec.ts +0 -447
  122. package/src/service/filter.ts +0 -337
  123. package/src/service/filter_integration.spec.ts +0 -825
  124. package/src/service/gcs_s3_utils.ts +0 -427
  125. package/src/service/manifest_service.spec.ts +0 -201
  126. package/src/service/manifest_service.ts +0 -106
  127. package/src/service/materialization_service.spec.ts +0 -648
  128. package/src/service/materialization_service.ts +0 -929
  129. package/src/service/materialized_table_gc.spec.ts +0 -383
  130. package/src/service/materialized_table_gc.ts +0 -279
  131. package/src/service/model.spec.ts +0 -257
  132. package/src/service/model.ts +0 -1069
  133. package/src/service/package.spec.ts +0 -236
  134. package/src/service/package.ts +0 -463
  135. package/src/service/project.ts +0 -681
  136. package/src/service/project_compile.spec.ts +0 -197
  137. package/src/service/project_store.spec.ts +0 -859
  138. package/src/service/project_store.ts +0 -1531
  139. package/src/service/quoting.ts +0 -41
  140. package/src/service/resolve_project.ts +0 -13
  141. package/src/storage/BaseRepository.ts +0 -31
  142. package/src/storage/DatabaseInterface.ts +0 -180
  143. package/src/storage/StorageManager.mock.ts +0 -59
  144. package/src/storage/StorageManager.ts +0 -210
  145. package/src/storage/duckdb/ConnectionRepository.ts +0 -141
  146. package/src/storage/duckdb/DuckDBConnection.ts +0 -177
  147. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  148. package/src/storage/duckdb/DuckDBRepository.ts +0 -224
  149. package/src/storage/duckdb/ManifestRepository.ts +0 -119
  150. package/src/storage/duckdb/MaterializationRepository.ts +0 -249
  151. package/src/storage/duckdb/PackageRepository.ts +0 -135
  152. package/src/storage/duckdb/ProjectRepository.ts +0 -148
  153. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  154. package/src/storage/duckdb/schema.ts +0 -147
  155. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
  156. package/src/utils.ts +0 -13
  157. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  158. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  159. package/tests/fixtures/persist-test/publisher.json +0 -5
  160. package/tests/fixtures/publisher.config.json +0 -15
  161. package/tests/harness/e2e.ts +0 -96
  162. package/tests/harness/mcp_test_setup.ts +0 -344
  163. package/tests/harness/mocks.ts +0 -37
  164. package/tests/harness/rest_e2e.ts +0 -68
  165. package/tests/harness/uris.ts +0 -31
  166. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
  167. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
  168. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
  169. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
  170. package/tests/integration/mcp/setup.spec.ts +0 -5
  171. package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
  172. package/tests/unit/ducklake/ducklake.test.ts +0 -950
  173. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  174. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
  175. package/tsconfig.json +0 -32
@@ -1,1069 +0,0 @@
1
- import { DuckDBConnection } from "@malloydata/db-duckdb";
2
- import {
3
- Annotation,
4
- API,
5
- Connection,
6
- FixedConnectionMap,
7
- isSourceDef,
8
- MalloyError,
9
- ModelDef,
10
- modelDefToModelInfo,
11
- ModelMaterializer,
12
- NamedModelObject,
13
- NamedQueryDef,
14
- QueryData,
15
- QueryMaterializer,
16
- Runtime,
17
- StructDef,
18
- TurtleDef,
19
- } from "@malloydata/malloy";
20
- import * as Malloy from "@malloydata/malloy-interfaces";
21
- import {
22
- MalloySQLParser,
23
- MalloySQLStatementType,
24
- } from "@malloydata/malloy-sql";
25
- import malloyPackage from "@malloydata/malloy/package.json";
26
- import { DataStyles } from "@malloydata/render";
27
- import { metrics } from "@opentelemetry/api";
28
- import * as fs from "fs/promises";
29
- import * as path from "path";
30
- import { fileURLToPath } from "url";
31
- import { components } from "../api";
32
- import {
33
- MODEL_FILE_SUFFIX,
34
- NOTEBOOK_FILE_SUFFIX,
35
- ROW_LIMIT,
36
- } from "../constants";
37
- import { HackyDataStylesAccumulator } from "../data_styles";
38
- import {
39
- BadRequestError,
40
- ModelCompilationError,
41
- ModelNotFoundError,
42
- } from "../errors";
43
- import { logger } from "../logger";
44
- import { BuildManifest } from "../storage/DatabaseInterface";
45
- import { URL_READER } from "../utils";
46
- import {
47
- buildFilterClause,
48
- injectFilterRefinement,
49
- parseFilters,
50
- FilterValidationError,
51
- type FilterDefinition,
52
- type FilterParams,
53
- } from "./filter";
54
-
55
- type ApiCompiledModel = components["schemas"]["CompiledModel"];
56
- type ApiNotebookCell = components["schemas"]["NotebookCell"];
57
- type ApiRawNotebook = components["schemas"]["RawNotebook"];
58
- type ApiSource = components["schemas"]["Source"];
59
- type ApiFilter = components["schemas"]["Filter"];
60
- type ApiView = components["schemas"]["View"];
61
- type ApiQuery = components["schemas"]["Query"];
62
- export type ApiConnection = components["schemas"]["Connection"];
63
- export type SnowflakeConnection = components["schemas"]["SnowflakeConnection"];
64
- export type PostgresConnection = components["schemas"]["PostgresConnection"];
65
- export type BigqueryConnection = components["schemas"]["BigqueryConnection"];
66
- export type TrinoConnection = components["schemas"]["TrinoConnection"];
67
-
68
- const MALLOY_VERSION = malloyPackage.version;
69
-
70
- export type ModelType = "model" | "notebook";
71
-
72
- interface RunnableNotebookCell {
73
- type: "code" | "markdown";
74
- text: string;
75
- runnable?: QueryMaterializer;
76
- /** Retained so we can rebuild the query with filter refinements at execution time. */
77
- modelMaterializer?: ModelMaterializer;
78
- newSources?: Malloy.SourceInfo[];
79
- queryInfo?: Malloy.QueryInfo;
80
- }
81
-
82
- export class Model {
83
- private packageName: string;
84
- private modelPath: string;
85
- private dataStyles: DataStyles;
86
- private modelType: ModelType;
87
- private modelMaterializer: ModelMaterializer | undefined;
88
- private modelDef: ModelDef | undefined;
89
- private modelInfo: Malloy.ModelInfo | undefined;
90
- private sources: ApiSource[] | undefined;
91
- private queries: ApiQuery[] | undefined;
92
- private sourceInfos: Malloy.SourceInfo[] | undefined;
93
- private runnableNotebookCells: RunnableNotebookCell[] | undefined;
94
- private compilationError: MalloyError | Error | undefined;
95
- /** Parsed #(filter) definitions keyed by source name. */
96
- private filterMap: Map<string, FilterDefinition[]>;
97
- private meter = metrics.getMeter("publisher");
98
- private queryExecutionHistogram = this.meter.createHistogram(
99
- "malloy_model_query_duration",
100
- {
101
- description: "How long it takes to execute a Malloy model query",
102
- unit: "ms",
103
- },
104
- );
105
-
106
- constructor(
107
- packageName: string,
108
- modelPath: string,
109
- dataStyles: DataStyles,
110
- modelType: ModelType,
111
- modelMaterializer: ModelMaterializer | undefined,
112
- modelDef: ModelDef | undefined,
113
- // TODO(jjs) - remove these
114
- sources: ApiSource[] | undefined,
115
- queries: ApiQuery[] | undefined,
116
- sourceInfos: Malloy.SourceInfo[] | undefined,
117
- runnableNotebookCells: RunnableNotebookCell[] | undefined,
118
- compilationError: MalloyError | Error | undefined,
119
- filterMap?: Map<string, FilterDefinition[]>,
120
- ) {
121
- this.packageName = packageName;
122
- this.modelPath = modelPath;
123
- this.dataStyles = dataStyles;
124
- this.modelType = modelType;
125
- this.modelDef = modelDef;
126
- this.modelMaterializer = modelMaterializer;
127
- this.sources = sources;
128
- this.queries = queries;
129
- this.sourceInfos = sourceInfos;
130
- this.runnableNotebookCells = runnableNotebookCells;
131
- this.compilationError = compilationError;
132
- this.filterMap = filterMap ?? new Map();
133
- this.modelInfo = this.modelDef
134
- ? modelDefToModelInfo(this.modelDef)
135
- : undefined;
136
- }
137
-
138
- /**
139
- * Get the parsed filter definitions for a given source name.
140
- * Returns an empty array if no filters are declared.
141
- */
142
- public getFilters(sourceName: string): FilterDefinition[] {
143
- return this.filterMap.get(sourceName) ?? [];
144
- }
145
-
146
- /**
147
- * Best-effort extraction of a source name from an ad-hoc Malloy query string.
148
- * Matches patterns like `run: source_name -> ...` or `source_name -> ...`.
149
- */
150
- private extractSourceName(query?: string): string | undefined {
151
- if (!query) return undefined;
152
- const runMatch = query.match(/run\s*:\s*(\w+)\s*->/);
153
- const arrowMatch = query.match(/^\s*(\w+)\s*->/m);
154
- return runMatch?.[1] ?? arrowMatch?.[1];
155
- }
156
-
157
- public static async create(
158
- packageName: string,
159
- packagePath: string,
160
- modelPath: string,
161
- connections: Map<string, Connection>,
162
- options?: { buildManifest?: BuildManifest["entries"] },
163
- ): Promise<Model> {
164
- // getModelRuntime might throw a ModelNotFoundError. It's the callers responsibility
165
- // to pass a valid model path or handle the error.
166
- const { runtime, modelURL, importBaseURL, dataStyles, modelType } =
167
- await Model.getModelRuntime(
168
- packagePath,
169
- modelPath,
170
- connections,
171
- options,
172
- );
173
-
174
- try {
175
- const { modelMaterializer, runnableNotebookCells } =
176
- await Model.getModelMaterializer(
177
- runtime,
178
- importBaseURL,
179
- modelURL,
180
- modelPath,
181
- );
182
-
183
- let modelDef = undefined;
184
- let sources = undefined;
185
- let queries = undefined;
186
- let filterMap: Map<string, FilterDefinition[]> | undefined;
187
- const sourceInfos: Malloy.SourceInfo[] = [];
188
- if (modelMaterializer) {
189
- modelDef = (await modelMaterializer.getModel())._modelDef;
190
- const sourceResult = Model.getSources(modelPath, modelDef);
191
- sources = sourceResult.sources;
192
- filterMap = sourceResult.filterMap;
193
- queries = Model.getQueries(modelPath, modelDef);
194
-
195
- // Collect sourceInfos from imported models first
196
- // This follows the same pattern as notebook imports handling
197
- const imports = modelDef.imports || [];
198
- const importedSourceNames = new Set<string>();
199
- for (const importLocation of imports) {
200
- try {
201
- const modelString = await runtime.urlReader.readURL(
202
- new URL(importLocation.importURL),
203
- );
204
- const importedModelDef = (
205
- await runtime
206
- .loadModel(modelString as string, { importBaseURL })
207
- .getModel()
208
- )._modelDef;
209
- const importedModelInfo =
210
- modelDefToModelInfo(importedModelDef);
211
- const importedSources = importedModelInfo.entries.filter(
212
- (entry) => entry.kind === "source",
213
- ) as Malloy.SourceInfo[];
214
- for (const source of importedSources) {
215
- if (!importedSourceNames.has(source.name)) {
216
- sourceInfos.push(source);
217
- importedSourceNames.add(source.name);
218
- }
219
- }
220
- } catch (importError) {
221
- // Log but don't fail if we can't load an import's sourceInfo
222
- logger.warn("Failed to load sourceInfo from import", {
223
- importURL: importLocation.importURL,
224
- error: importError,
225
- });
226
- }
227
- }
228
-
229
- // Add locally-defined sources (not already added from imports)
230
- const localModelInfo = modelDefToModelInfo(modelDef);
231
- const localSources = localModelInfo.entries.filter(
232
- (entry) => entry.kind === "source",
233
- ) as Malloy.SourceInfo[];
234
- for (const source of localSources) {
235
- if (!importedSourceNames.has(source.name)) {
236
- sourceInfos.push(source);
237
- }
238
- }
239
- }
240
-
241
- return new Model(
242
- packageName,
243
- modelPath,
244
- dataStyles,
245
- modelType,
246
- modelMaterializer,
247
- modelDef,
248
- sources,
249
- queries,
250
- sourceInfos.length > 0 ? sourceInfos : undefined,
251
- runnableNotebookCells,
252
- undefined,
253
- filterMap,
254
- );
255
- } catch (error) {
256
- let computedError = error;
257
- if (error instanceof Error && error.stack) {
258
- logger.error("Error stack", error.stack);
259
- }
260
-
261
- if (error instanceof MalloyError) {
262
- const problems = error.problems;
263
- for (const problem of problems) {
264
- logger.error("Problem", problem);
265
- }
266
- computedError = new ModelCompilationError(error);
267
- }
268
- return new Model(
269
- packageName,
270
- modelPath,
271
- dataStyles,
272
- modelType,
273
- undefined,
274
- undefined,
275
- undefined,
276
- undefined,
277
- undefined,
278
- undefined,
279
- computedError as Error,
280
- );
281
- }
282
- }
283
-
284
- public getPath(): string {
285
- return this.modelPath;
286
- }
287
-
288
- public getType(): ModelType {
289
- return this.modelType;
290
- }
291
-
292
- public getSources(): ApiSource[] | undefined {
293
- return this.sources;
294
- }
295
-
296
- public getSourceInfos(): Malloy.SourceInfo[] | undefined {
297
- return this.sourceInfos;
298
- }
299
-
300
- public getQueries(): ApiQuery[] | undefined {
301
- return this.queries;
302
- }
303
-
304
- public async getModel(): Promise<ApiCompiledModel> {
305
- if (this.compilationError) {
306
- throw this.compilationError;
307
- }
308
-
309
- if (this.modelType === "model") {
310
- return this.getStandardModel();
311
- } else {
312
- throw new ModelNotFoundError(
313
- `${this.modelPath} is not a valid model name. Model files must end in .malloy.`,
314
- );
315
- }
316
- }
317
-
318
- public getNotebookError(): MalloyError | Error | undefined {
319
- return this.compilationError;
320
- }
321
-
322
- public async getNotebook(): Promise<ApiRawNotebook> {
323
- if (this.compilationError) {
324
- throw this.compilationError;
325
- }
326
- if (this.modelType === "notebook") {
327
- return this.getNotebookModel();
328
- } else {
329
- throw new ModelNotFoundError(
330
- `${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb.`,
331
- );
332
- }
333
- }
334
-
335
- public async getQueryResults(
336
- sourceName?: string,
337
- queryName?: string,
338
- query?: string,
339
- filterParams?: FilterParams,
340
- bypassFilters?: boolean,
341
- ): Promise<{
342
- result: Malloy.Result;
343
- compactResult: QueryData;
344
- modelInfo: Malloy.ModelInfo;
345
- dataStyles: DataStyles;
346
- }> {
347
- const startTime = performance.now();
348
- if (this.compilationError) {
349
- // Re-throw MalloyError and ModelCompilationError as-is (they map to 400/424)
350
- if (
351
- this.compilationError instanceof MalloyError ||
352
- this.compilationError instanceof ModelCompilationError
353
- ) {
354
- throw this.compilationError;
355
- }
356
- // For other compilation errors, wrap as BadRequestError (400)
357
- throw new BadRequestError(
358
- `Model compilation failed: ${this.compilationError.message}`,
359
- );
360
- }
361
- let runnable: QueryMaterializer;
362
- if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
363
- throw new BadRequestError("Model has no queryable entities.");
364
-
365
- // Wrap loadQuery calls in try-catch to handle query parsing errors
366
- try {
367
- let queryString: string;
368
- if (!sourceName && !queryName && query) {
369
- queryString = "\n" + query;
370
- } else if (queryName && !query) {
371
- queryString = `\nrun: ${sourceName ? sourceName + "->" : ""}${queryName}`;
372
- } else {
373
- const endTime = performance.now();
374
- const executionTime = endTime - startTime;
375
- this.queryExecutionHistogram.record(executionTime, {
376
- "malloy.model.path": this.modelPath,
377
- "malloy.model.query.name": queryName,
378
- "malloy.model.query.source": sourceName,
379
- "malloy.model.query.query": query,
380
- "malloy.model.query.status": "error",
381
- });
382
- throw new BadRequestError(
383
- "Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.",
384
- );
385
- }
386
-
387
- // Inject source filter predicates unless bypassed
388
- if (!bypassFilters) {
389
- const effectiveSource = sourceName ?? this.extractSourceName(query);
390
- if (effectiveSource) {
391
- const filters = this.getFilters(effectiveSource);
392
- if (filters.length > 0) {
393
- const filterClause = buildFilterClause(
394
- filters,
395
- filterParams ?? {},
396
- );
397
- queryString = injectFilterRefinement(
398
- queryString,
399
- filterClause,
400
- );
401
- }
402
- }
403
- }
404
-
405
- runnable = this.modelMaterializer.loadQuery(queryString);
406
- } catch (error) {
407
- // Re-throw BadRequestError as-is
408
- if (error instanceof BadRequestError) {
409
- throw error;
410
- }
411
- // Source filter validation errors are client errors (400)
412
- if (error instanceof FilterValidationError) {
413
- throw new BadRequestError(error.message);
414
- }
415
- // Re-throw MalloyError as-is (maps to 400)
416
- if (error instanceof MalloyError) {
417
- throw error;
418
- }
419
- // For other query parsing errors, wrap as BadRequestError
420
- const errorMessage =
421
- error instanceof Error ? error.message : String(error);
422
- logger.error("Query parsing error", {
423
- error,
424
- errorMessage,
425
- projectName: this.packageName,
426
- modelPath: this.modelPath,
427
- query,
428
- queryName,
429
- sourceName,
430
- });
431
- throw new BadRequestError(`Invalid query: ${errorMessage}`);
432
- }
433
-
434
- const rowLimit =
435
- (await runnable.getPreparedResult()).resultExplore.limit || ROW_LIMIT;
436
- const endTime = performance.now();
437
- const executionTime = endTime - startTime;
438
-
439
- let queryResults;
440
- try {
441
- queryResults = await runnable.run({ rowLimit });
442
- } catch (error) {
443
- // Record error metrics
444
- const errorEndTime = performance.now();
445
- const errorExecutionTime = errorEndTime - startTime;
446
- this.queryExecutionHistogram.record(errorExecutionTime, {
447
- "malloy.model.path": this.modelPath,
448
- "malloy.model.query.name": queryName,
449
- "malloy.model.query.source": sourceName,
450
- "malloy.model.query.query": query,
451
- "malloy.model.query.status": "error",
452
- });
453
-
454
- // Re-throw Malloy errors as-is (they will be handled by error handler)
455
- if (error instanceof MalloyError) {
456
- throw error;
457
- }
458
-
459
- // For other runtime errors (like divide by zero), throw as BadRequestError
460
- const errorMessage =
461
- error instanceof Error ? error.message : String(error);
462
- logger.error("Query execution error", {
463
- error,
464
- errorMessage,
465
- projectName: this.packageName,
466
- modelPath: this.modelPath,
467
- query,
468
- queryName,
469
- sourceName,
470
- });
471
- throw new BadRequestError(`Query execution failed: ${errorMessage}`);
472
- }
473
-
474
- this.queryExecutionHistogram.record(executionTime, {
475
- "malloy.model.path": this.modelPath,
476
- "malloy.model.query.name": queryName,
477
- "malloy.model.query.source": sourceName,
478
- "malloy.model.query.query": query,
479
- "malloy.model.query.rows_limit": rowLimit,
480
- "malloy.model.query.rows_total": queryResults.totalRows,
481
- "malloy.model.query.connection": queryResults.connectionName,
482
- "malloy.model.query.status": "success",
483
- });
484
- return {
485
- result: API.util.wrapResult(queryResults),
486
- compactResult: queryResults.data.value,
487
- modelInfo: this.modelInfo,
488
- dataStyles: this.dataStyles,
489
- };
490
- }
491
-
492
- private getStandardModel(): ApiCompiledModel {
493
- return {
494
- type: "source",
495
- packageName: this.packageName,
496
- modelPath: this.modelPath,
497
- malloyVersion: MALLOY_VERSION,
498
- dataStyles: JSON.stringify(this.dataStyles),
499
- modelDef: JSON.stringify(this.modelDef),
500
- modelInfo: JSON.stringify(
501
- this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
502
- ),
503
- sourceInfos: this.getSourceInfos()?.map((sourceInfo) =>
504
- JSON.stringify(sourceInfo),
505
- ),
506
- sources: this.sources,
507
- queries: this.queries,
508
- } as ApiCompiledModel;
509
- }
510
-
511
- private async getNotebookModel(): Promise<ApiRawNotebook> {
512
- // Return raw cell contents without executing them
513
- const notebookCells: ApiNotebookCell[] = (
514
- this.runnableNotebookCells as RunnableNotebookCell[]
515
- ).map((cell) => {
516
- logger.debug("cell.queryInfo", cell.queryInfo);
517
- return {
518
- type: cell.type,
519
- text: cell.text,
520
- newSources: cell.newSources?.map((source) =>
521
- JSON.stringify(source),
522
- ),
523
- queryInfo: cell.queryInfo
524
- ? JSON.stringify(cell.queryInfo)
525
- : undefined,
526
- } as ApiNotebookCell;
527
- });
528
-
529
- // Collect all annotations from the inherits chain
530
- const allAnnotations: string[] = [];
531
- if (this.modelDef) {
532
- // Traverse the inherits chain to collect all annotations
533
- // Type as Annotation to handle the inherits chain properly
534
- let currentAnnotation: Annotation | undefined =
535
- this.modelDef.annotation;
536
-
537
- while (currentAnnotation) {
538
- if (currentAnnotation.notes) {
539
- allAnnotations.push(
540
- ...currentAnnotation.notes.map((note) => note.text),
541
- );
542
- }
543
- // Navigate to the inherited annotation if it exists
544
- currentAnnotation = currentAnnotation.inherits;
545
- }
546
- }
547
-
548
- return {
549
- type: "notebook",
550
- packageName: this.packageName,
551
- modelPath: this.modelPath,
552
- malloyVersion: MALLOY_VERSION,
553
- modelInfo: JSON.stringify(
554
- this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
555
- ),
556
- sources: this.modelDef && this.sources,
557
- queries: this.modelDef && this.queries,
558
- annotations: allAnnotations,
559
- notebookCells,
560
- } as ApiRawNotebook;
561
- }
562
-
563
- public async executeNotebookCell(
564
- cellIndex: number,
565
- filterParams?: FilterParams,
566
- bypassFilters?: boolean,
567
- ): Promise<{
568
- type: "code" | "markdown";
569
- text: string;
570
- queryName?: string;
571
- result?: string;
572
- newSources?: string[];
573
- }> {
574
- if (this.compilationError) {
575
- throw this.compilationError;
576
- }
577
-
578
- if (!this.runnableNotebookCells) {
579
- throw new BadRequestError("No notebook cells available");
580
- }
581
-
582
- if (cellIndex < 0 || cellIndex >= this.runnableNotebookCells.length) {
583
- throw new BadRequestError(
584
- `Cell index ${cellIndex} out of range (0-${this.runnableNotebookCells.length - 1})`,
585
- );
586
- }
587
-
588
- const cell = this.runnableNotebookCells[cellIndex];
589
-
590
- if (cell.type === "markdown") {
591
- return {
592
- type: cell.type,
593
- text: cell.text,
594
- };
595
- }
596
-
597
- // For code cells, execute the runnable if available
598
- let queryName: string | undefined = undefined;
599
- let queryResult: string | undefined = undefined;
600
-
601
- if (cell.runnable) {
602
- try {
603
- let runnableToExecute = cell.runnable;
604
-
605
- // If filters need to be applied, rebuild the query with a refinement
606
- if (!bypassFilters && cell.modelMaterializer) {
607
- const effectiveSource = this.extractSourceName(cell.text);
608
- if (effectiveSource) {
609
- const filters = this.getFilters(effectiveSource);
610
- if (filters.length > 0) {
611
- const filterClause = buildFilterClause(
612
- filters,
613
- filterParams ?? {},
614
- );
615
- if (filterClause) {
616
- const refinedQuery = injectFilterRefinement(
617
- cell.text,
618
- filterClause,
619
- );
620
- runnableToExecute =
621
- cell.modelMaterializer.loadQuery(refinedQuery);
622
- }
623
- }
624
- }
625
- }
626
-
627
- const rowLimit =
628
- (await runnableToExecute.getPreparedResult()).resultExplore
629
- .limit || ROW_LIMIT;
630
- const result = await runnableToExecute.run({ rowLimit });
631
- const query = (await runnableToExecute.getPreparedQuery())._query;
632
- queryName = (query as NamedQueryDef).as || query.name;
633
- queryResult =
634
- result?._queryResult &&
635
- this.modelInfo &&
636
- JSON.stringify(API.util.wrapResult(result));
637
- } catch (error) {
638
- if (error instanceof FilterValidationError) {
639
- throw new BadRequestError(error.message);
640
- }
641
- if (error instanceof MalloyError) {
642
- throw error;
643
- }
644
- const errorMessage =
645
- error instanceof Error ? error.message : String(error);
646
- if (errorMessage.trim() === "Model has no queries.") {
647
- return {
648
- type: "code",
649
- text: cell.text,
650
- };
651
- } else {
652
- logger.error("Error message: ", errorMessage);
653
- }
654
- logger.debug("Cell content: ", cellIndex, cell.type, cell.text);
655
- throw new BadRequestError(`Cell execution failed: ${errorMessage}`);
656
- }
657
- }
658
-
659
- return {
660
- type: cell.type,
661
- text: cell.text,
662
- queryName: queryName,
663
- result: queryResult,
664
- newSources: cell.newSources?.map((source) => JSON.stringify(source)),
665
- };
666
- }
667
-
668
- static async getModelRuntime(
669
- packagePath: string,
670
- modelPath: string,
671
- connections: Map<string, Connection>,
672
- options?: { buildManifest?: BuildManifest["entries"] },
673
- ): Promise<{
674
- runtime: Runtime;
675
- modelURL: URL;
676
- importBaseURL: URL;
677
- dataStyles: DataStyles;
678
- modelType: ModelType;
679
- }> {
680
- const fullModelPath = path.join(packagePath, modelPath);
681
- try {
682
- if (!(await fs.stat(fullModelPath)).isFile()) {
683
- throw new ModelNotFoundError(`${modelPath} is not a file.`);
684
- }
685
- } catch {
686
- throw new ModelNotFoundError(`${modelPath} does not exist.`);
687
- }
688
-
689
- let modelType: ModelType;
690
- if (modelPath.endsWith(MODEL_FILE_SUFFIX)) {
691
- modelType = "model";
692
- } else if (modelPath.endsWith(NOTEBOOK_FILE_SUFFIX)) {
693
- modelType = "notebook";
694
- } else {
695
- throw new ModelNotFoundError(
696
- `${modelPath} is not a valid model name. Model files must end in .malloy or .malloynb.`,
697
- );
698
- }
699
-
700
- const modelURL = new URL(`file://${fullModelPath}`);
701
- const baseUrl = new URL(".", modelURL);
702
- const fileUrl = new URL(baseUrl.pathname, "file:");
703
- const workingDirectory = fileURLToPath(fileUrl);
704
- const importBaseURL = new URL(baseUrl.pathname + "/", "file:");
705
- const urlReader = new HackyDataStylesAccumulator(URL_READER);
706
-
707
- const duckdbConnection = connections.get("duckdb") as DuckDBConnection;
708
- await duckdbConnection.runSQL(
709
- `SET FILE_SEARCH_PATH='${workingDirectory}';`,
710
- );
711
-
712
- const runtimeOptions: {
713
- urlReader: typeof urlReader;
714
- connections: FixedConnectionMap;
715
- buildManifest?: BuildManifest;
716
- } = {
717
- urlReader,
718
- connections: new FixedConnectionMap(connections, "duckdb"),
719
- };
720
-
721
- if (options?.buildManifest) {
722
- runtimeOptions.buildManifest = {
723
- entries: options.buildManifest,
724
- strict: false,
725
- };
726
- }
727
-
728
- const runtime = new Runtime(runtimeOptions);
729
- const dataStyles = urlReader.getHackyAccumulatedDataStyles();
730
- return { runtime, modelURL, importBaseURL, dataStyles, modelType };
731
- }
732
-
733
- private static getQueries(
734
- modelPath: string,
735
- modelDef: ModelDef,
736
- ): ApiQuery[] {
737
- const isNamedQuery = (
738
- object: NamedModelObject,
739
- ): object is NamedQueryDef => object.type === "query";
740
- return Object.values(modelDef.contents)
741
- .filter(isNamedQuery)
742
- .map((queryObj: NamedQueryDef) => ({
743
- name: queryObj.as || queryObj.name,
744
- // What to do when the source is not a string?
745
- sourceName:
746
- typeof queryObj.structRef === "string"
747
- ? queryObj.structRef
748
- : undefined,
749
- annotations: queryObj?.annotation?.blockNotes
750
- ?.filter((note: { at: { url: string } }) =>
751
- note.at.url.includes(modelPath),
752
- )
753
- .map((note: { text: string }) => note.text),
754
- }));
755
- }
756
-
757
- private static getSources(
758
- modelPath: string,
759
- modelDef: ModelDef,
760
- ): {
761
- sources: ApiSource[];
762
- filterMap: Map<string, FilterDefinition[]>;
763
- } {
764
- const filterMap = new Map<string, FilterDefinition[]>();
765
-
766
- const sources = Object.values(modelDef.contents)
767
- .filter((obj) => isSourceDef(obj))
768
- .map((sourceObj) => {
769
- const sourceName = sourceObj.as || sourceObj.name;
770
- const annotations = (sourceObj as StructDef).annotation?.blockNotes
771
- ?.filter((note) => note.at.url.includes(modelPath))
772
- .map((note) => note.text);
773
-
774
- // Parse #(filter) from ALL annotations, traversing the inherits
775
- // chain so that filters on a base source (e.g. `recalls`) are
776
- // picked up by an extending source (`manufacturer_recalls is
777
- // recalls extend {}`). The Malloy compiler stores the base
778
- // source's annotations in `annotation.inherits`.
779
- //
780
- // The chain goes child → parent, so we collect child-first.
781
- // parseFilters uses "last wins" dedup, so we reverse to put
782
- // parent annotations first and child annotations last (winning).
783
- const collectedAnnotations: string[][] = [];
784
- let curAnnotation: Annotation | undefined = (sourceObj as StructDef)
785
- .annotation;
786
- while (curAnnotation) {
787
- if (curAnnotation.blockNotes) {
788
- collectedAnnotations.push(
789
- curAnnotation.blockNotes.map((note) => note.text),
790
- );
791
- }
792
- curAnnotation = curAnnotation.inherits;
793
- }
794
- const allAnnotations = collectedAnnotations.reverse().flat();
795
- let filters: ApiFilter[] | undefined;
796
- if (allAnnotations.length > 0) {
797
- try {
798
- const parsed = parseFilters(allAnnotations);
799
- if (parsed.length > 0) {
800
- filterMap.set(sourceName, parsed);
801
- const structFields = (sourceObj as StructDef).fields;
802
- filters = parsed.map((f) => {
803
- const field = structFields.find(
804
- (fd) => (fd.as || fd.name) === f.dimension,
805
- );
806
- return {
807
- name: f.name,
808
- dimension: f.dimension,
809
- type: f.type,
810
- implicit: f.implicit,
811
- required: f.required,
812
- dimensionType: field?.type as string | undefined,
813
- };
814
- });
815
- }
816
- } catch (err) {
817
- logger.warn(
818
- `Failed to parse filter annotations on source "${sourceName}"`,
819
- { error: err },
820
- );
821
- }
822
- }
823
-
824
- const views = (sourceObj as StructDef).fields
825
- .filter((turtleObj) => turtleObj.type === "turtle")
826
- .filter((turtleObj) =>
827
- // TODO(kjnesbit): Fix non-reduce views. Filter out
828
- // non-reduce views, i.e., indexes. Need to discuss with Will.
829
- (turtleObj as TurtleDef).pipeline
830
- .map((stage) => stage.type)
831
- .every((type) => type == "reduce"),
832
- )
833
- .map(
834
- (turtleObj) =>
835
- ({
836
- name: turtleObj.as || turtleObj.name,
837
- annotations: turtleObj?.annotation?.blockNotes
838
- ?.filter((note) => note.at.url.includes(modelPath))
839
- .map((note) => note.text),
840
- }) as ApiView,
841
- );
842
-
843
- return {
844
- name: sourceName,
845
- annotations,
846
- views,
847
- filters,
848
- } as ApiSource;
849
- });
850
-
851
- return { sources, filterMap };
852
- }
853
-
854
- static async getModelMaterializer(
855
- runtime: Runtime,
856
- importBaseURL: URL,
857
- modelURL: URL,
858
- modelPath: string,
859
- ): Promise<{
860
- modelMaterializer: ModelMaterializer | undefined;
861
- runnableNotebookCells: RunnableNotebookCell[] | undefined;
862
- }> {
863
- if (modelPath.endsWith(MODEL_FILE_SUFFIX)) {
864
- const modelMaterializer = await Model.getStandardModelMaterializer(
865
- runtime,
866
- importBaseURL,
867
- modelURL,
868
- modelPath,
869
- );
870
- return {
871
- modelMaterializer,
872
- runnableNotebookCells: undefined,
873
- };
874
- } else if (modelPath.endsWith(NOTEBOOK_FILE_SUFFIX)) {
875
- const { modelMaterializer: mm, runnableNotebookCells: rnc } =
876
- await Model.getNotebookModelMaterializer(
877
- runtime,
878
- importBaseURL,
879
- modelURL,
880
- modelPath,
881
- );
882
- return {
883
- modelMaterializer: mm,
884
- runnableNotebookCells: rnc,
885
- };
886
- } else {
887
- throw new Error(
888
- `${modelPath} is not a valid model name. Model files must end in .malloy or .malloynb.`,
889
- );
890
- }
891
- }
892
-
893
- private static async getStandardModelMaterializer(
894
- runtime: Runtime,
895
- importBaseURL: URL,
896
- modelURL: URL,
897
- modelPath: string,
898
- ): Promise<ModelMaterializer> {
899
- const mm = runtime.loadModel(modelURL, { importBaseURL });
900
- if (!mm) {
901
- throw new Error(`Invalid model ${modelPath}.`);
902
- }
903
- return mm;
904
- }
905
-
906
- private static async getNotebookModelMaterializer(
907
- runtime: Runtime,
908
- importBaseURL: URL,
909
- modelURL: URL,
910
- modelPath: string,
911
- ): Promise<{
912
- modelMaterializer: ModelMaterializer | undefined;
913
- runnableNotebookCells: RunnableNotebookCell[];
914
- }> {
915
- let fileContents = undefined;
916
- let parse = undefined;
917
-
918
- try {
919
- fileContents = await fs.readFile(modelURL, "utf8");
920
- } catch {
921
- throw new ModelNotFoundError("Model not found: " + modelPath);
922
- }
923
-
924
- try {
925
- parse = MalloySQLParser.parse(fileContents, modelPath);
926
- } catch {
927
- throw new Error("Could not parse model: " + modelPath);
928
- }
929
-
930
- let mm: ModelMaterializer | undefined = undefined;
931
- const oldImports: string[] = [];
932
- const oldSources: Record<string, Malloy.SourceInfo> = {};
933
- // First generate the sequence of ModelMaterializers.
934
- // This has to happen sync, since mm.getModel() is async and
935
- // may execute out-of-order.
936
- const mms = parse.statements.map((stmt) => {
937
- if (stmt.type === MalloySQLStatementType.MALLOY) {
938
- if (!mm) {
939
- mm = runtime.loadModel(stmt.text, { importBaseURL });
940
- } else {
941
- mm = mm.extendModel(stmt.text, { importBaseURL });
942
- }
943
- }
944
- return mm;
945
- });
946
- const runnableNotebookCells: RunnableNotebookCell[] = (
947
- await Promise.all(
948
- parse.statements.map(async (stmt, index) => {
949
- if (stmt.type === MalloySQLStatementType.MALLOY) {
950
- // Get the Materializer for the current cell/statement.
951
- const localMM = mms[index];
952
- if (!localMM) {
953
- // This can't happen because the to be in this branch there stmt must be
954
- // MalloySQLStatementType.MALLOY and we must have a model materializer.
955
- throw new Error("Model materializer is undefined");
956
- }
957
- // Pull available sources from the current model.
958
- // Add any of then that are new into newSources and then add them to oldSources.
959
- const currentModelDef = (await localMM.getModel())._modelDef;
960
- let newSources: Malloy.SourceInfo[] = [];
961
- const newImports = currentModelDef.imports?.slice(
962
- oldImports.length,
963
- );
964
- if (newImports) {
965
- await Promise.all(
966
- newImports.map(async (importLocation) => {
967
- const modelString = await runtime.urlReader.readURL(
968
- new URL(importLocation.importURL),
969
- );
970
- const importModel = (
971
- await runtime
972
- .loadModel(modelString as string, {
973
- importBaseURL,
974
- })
975
- .getModel()
976
- )._modelDef;
977
- const importModelInfo =
978
- modelDefToModelInfo(importModel);
979
- newSources = importModelInfo.entries
980
- .filter((entry) => entry.kind === "source")
981
- .filter(
982
- (source) => !(source.name in oldSources),
983
- ) as Malloy.SourceInfo[];
984
- oldImports.push(importLocation.importURL.toString());
985
- }),
986
- );
987
- }
988
- const currentModelInfo = modelDefToModelInfo(currentModelDef);
989
- newSources = newSources.concat(
990
- currentModelInfo.entries
991
- .filter((entry) => entry.kind === "source")
992
- .filter(
993
- (source) => !(source.name in oldSources),
994
- ) as Malloy.SourceInfo[],
995
- );
996
-
997
- for (const source of newSources) {
998
- oldSources[source.name] = source;
999
- }
1000
-
1001
- const runnable = localMM.loadFinalQuery();
1002
-
1003
- // Extract QueryInfo from the runnable
1004
- let queryInfo: Malloy.QueryInfo | undefined = undefined;
1005
- try {
1006
- const preparedQuery = await runnable.getPreparedQuery();
1007
- const query = preparedQuery._query as NamedQueryDef;
1008
- const queryName = query.as || query.name;
1009
- const anonymousQuery =
1010
- currentModelInfo.anonymous_queries[
1011
- currentModelInfo.anonymous_queries.length - 1
1012
- ];
1013
-
1014
- if (anonymousQuery) {
1015
- queryInfo = {
1016
- name: queryName,
1017
- schema: anonymousQuery.schema,
1018
- annotations: anonymousQuery.annotations,
1019
- definition: anonymousQuery.definition,
1020
- code: anonymousQuery.code,
1021
- location: anonymousQuery.location,
1022
- } as Malloy.QueryInfo;
1023
- }
1024
- } catch (_error) {
1025
- // If we can't extract query info (e.g., no query in cell), that's okay
1026
- // This can happen for cells that only define sources
1027
- }
1028
-
1029
- return {
1030
- type: "code",
1031
- text: stmt.text,
1032
- runnable: runnable,
1033
- modelMaterializer: localMM,
1034
- newSources,
1035
- queryInfo,
1036
- } as RunnableNotebookCell;
1037
- } else if (stmt.type === MalloySQLStatementType.MARKDOWN) {
1038
- return {
1039
- type: "markdown",
1040
- text: stmt.text,
1041
- } as RunnableNotebookCell;
1042
- } else {
1043
- return undefined;
1044
- }
1045
- }),
1046
- )
1047
- ).filter((cell) => cell !== undefined);
1048
-
1049
- return {
1050
- modelMaterializer: mm,
1051
- runnableNotebookCells: runnableNotebookCells,
1052
- };
1053
- }
1054
-
1055
- public getModelType(): ModelType {
1056
- return this.modelType;
1057
- }
1058
-
1059
- public async getFileText(packagePath: string): Promise<string> {
1060
- const fullPath = path.join(packagePath, this.modelPath);
1061
- try {
1062
- return await fs.readFile(fullPath, "utf8");
1063
- } catch {
1064
- throw new ModelNotFoundError(
1065
- `Model file not found: ${this.modelPath}`,
1066
- );
1067
- }
1068
- }
1069
- }