@malloy-publisher/server 0.0.235 → 0.0.236
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 +47 -17
- package/dist/package_load_worker.mjs +8 -1
- package/dist/server.mjs +471 -54
- package/package.json +5 -2
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -97
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.example.bigquery.json +0 -33
- package/publisher.config.example.duckdb.json +0 -23
- package/publisher.config.json +0 -23
- package/scripts/bake-duckdb-extensions.js +0 -110
- package/src/config.spec.ts +0 -1535
- package/src/config.theme.spec.ts +0 -178
- package/src/config.ts +0 -1142
- package/src/constants.ts +0 -105
- package/src/controller/compile.controller.ts +0 -40
- package/src/controller/connection.controller.spec.ts +0 -849
- package/src/controller/connection.controller.ts +0 -901
- package/src/controller/database.controller.ts +0 -24
- package/src/controller/materialization.controller.spec.ts +0 -293
- package/src/controller/materialization.controller.ts +0 -319
- package/src/controller/model.controller.ts +0 -164
- package/src/controller/package.controller.spec.ts +0 -273
- package/src/controller/package.controller.ts +0 -306
- package/src/controller/query.controller.ts +0 -152
- package/src/controller/theme.controller.ts +0 -83
- package/src/controller/watch-mode.controller.ts +0 -243
- package/src/data_styles.ts +0 -71
- package/src/default-publisher.config.json +0 -23
- package/src/ducklake_version.spec.ts +0 -163
- package/src/ducklake_version.ts +0 -153
- package/src/errors.spec.ts +0 -119
- package/src/errors.ts +0 -237
- package/src/filter_deprecation.spec.ts +0 -64
- package/src/filter_deprecation.ts +0 -42
- package/src/health.spec.ts +0 -123
- package/src/health.ts +0 -233
- package/src/heap_check.spec.ts +0 -144
- package/src/heap_check.ts +0 -144
- package/src/instrumentation.ts +0 -214
- package/src/json_utils.spec.ts +0 -51
- package/src/json_utils.ts +0 -33
- package/src/logger.spec.ts +0 -210
- package/src/logger.ts +0 -251
- package/src/malloy_pin_prereqs.spec.ts +0 -25
- package/src/materialization_metrics.spec.ts +0 -169
- package/src/materialization_metrics.ts +0 -346
- package/src/mcp/error_messages.spec.ts +0 -35
- package/src/mcp/error_messages.ts +0 -183
- package/src/mcp/handler_utils.spec.ts +0 -108
- package/src/mcp/handler_utils.ts +0 -251
- package/src/mcp/mcp_constants.ts +0 -6
- package/src/mcp/query_envelope.spec.ts +0 -229
- package/src/mcp/query_envelope.ts +0 -240
- package/src/mcp/server.protocol.spec.ts +0 -250
- package/src/mcp/server.ts +0 -87
- package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
- package/src/mcp/skills/build_skills_bundle.ts +0 -176
- package/src/mcp/skills/skills_bundle.json +0 -1
- package/src/mcp/skills/skills_bundle.spec.ts +0 -171
- package/src/mcp/tool_response.spec.ts +0 -108
- package/src/mcp/tool_response.ts +0 -138
- package/src/mcp/tools/compile_tool.spec.ts +0 -315
- package/src/mcp/tools/compile_tool.ts +0 -208
- package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
- package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
- package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
- package/src/mcp/tools/docs_search_tool.ts +0 -128
- package/src/mcp/tools/embedding_index.spec.ts +0 -1236
- package/src/mcp/tools/embedding_index.ts +0 -808
- package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
- package/src/mcp/tools/execute_query_tool.ts +0 -316
- package/src/mcp/tools/get_context_eval.ts +0 -275
- package/src/mcp/tools/get_context_tool.spec.ts +0 -592
- package/src/mcp/tools/get_context_tool.ts +0 -585
- package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
- package/src/mcp/tools/reload_package_tool.ts +0 -132
- package/src/mcp_config.spec.ts +0 -919
- package/src/mcp_config.ts +0 -425
- package/src/oom_guards.integration.spec.ts +0 -269
- package/src/package_load/package_load_pool.spec.ts +0 -252
- package/src/package_load/package_load_pool.ts +0 -931
- package/src/package_load/package_load_worker.ts +0 -1034
- package/src/package_load/protocol.ts +0 -364
- package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
- package/src/package_load/rpc_wait_accountant.ts +0 -76
- package/src/package_load_metrics.spec.ts +0 -114
- package/src/package_load_metrics.ts +0 -127
- package/src/path_safety.spec.ts +0 -158
- package/src/path_safety.ts +0 -146
- package/src/pg_helpers.spec.ts +0 -223
- package/src/pg_helpers.ts +0 -52
- package/src/query_cap_metrics.spec.ts +0 -89
- package/src/query_cap_metrics.ts +0 -116
- package/src/query_concurrency.spec.ts +0 -247
- package/src/query_concurrency.ts +0 -237
- package/src/query_metadata_metrics.ts +0 -49
- package/src/query_param_utils.ts +0 -29
- package/src/query_timeout.spec.ts +0 -224
- package/src/query_timeout.ts +0 -179
- package/src/runtime/publisher.js +0 -328
- package/src/server-old.ts +0 -988
- package/src/server.ts +0 -2059
- package/src/service/annotations.spec.ts +0 -118
- package/src/service/annotations.ts +0 -91
- package/src/service/authorize.spec.ts +0 -154
- package/src/service/authorize.ts +0 -498
- package/src/service/authorize_integration.spec.ts +0 -2041
- package/src/service/build_plan.spec.ts +0 -685
- package/src/service/build_plan.ts +0 -797
- package/src/service/compile_authorize.spec.ts +0 -85
- package/src/service/compile_fragment_techniques.spec.ts +0 -156
- package/src/service/connection.spec.ts +0 -2360
- package/src/service/connection.ts +0 -2187
- package/src/service/connection_config.spec.ts +0 -734
- package/src/service/connection_config.ts +0 -899
- package/src/service/connection_federation.spec.ts +0 -184
- package/src/service/connection_fingerprint.spec.ts +0 -102
- package/src/service/connection_service.spec.ts +0 -711
- package/src/service/connection_service.ts +0 -291
- package/src/service/cron_evaluator.spec.ts +0 -79
- package/src/service/cron_evaluator.ts +0 -105
- package/src/service/db_utils.spec.ts +0 -1085
- package/src/service/db_utils.ts +0 -1502
- package/src/service/duckdb_instance_isolation.spec.ts +0 -137
- package/src/service/ducklake_lazy_attach.spec.ts +0 -110
- package/src/service/embedding_provider.spec.ts +0 -329
- package/src/service/embedding_provider.ts +0 -236
- package/src/service/environment.ts +0 -2191
- package/src/service/environment_admission.spec.ts +0 -344
- package/src/service/environment_compile.spec.ts +0 -197
- package/src/service/environment_store.spec.ts +0 -2274
- package/src/service/environment_store.ts +0 -2434
- package/src/service/environment_store_anchoring.spec.ts +0 -107
- package/src/service/environment_store_clone.spec.ts +0 -350
- package/src/service/explore_visibility.spec.ts +0 -434
- package/src/service/exports_probe.spec.ts +0 -107
- package/src/service/extension_fetch_policy.spec.ts +0 -256
- package/src/service/filter.spec.ts +0 -458
- package/src/service/filter.ts +0 -341
- package/src/service/filter_bypass.spec.ts +0 -418
- package/src/service/filter_integration.spec.ts +0 -1004
- package/src/service/freshness.spec.ts +0 -183
- package/src/service/freshness.ts +0 -112
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/given.ts +0 -105
- package/src/service/givens_integration.spec.ts +0 -219
- package/src/service/manifest_loader.spec.ts +0 -189
- package/src/service/manifest_loader.ts +0 -156
- package/src/service/materialization_build_session.spec.ts +0 -435
- package/src/service/materialization_build_session.ts +0 -681
- package/src/service/materialization_config_validation.spec.ts +0 -99
- package/src/service/materialization_config_validation.ts +0 -120
- package/src/service/materialization_eligibility.spec.ts +0 -158
- package/src/service/materialization_eligibility.ts +0 -305
- package/src/service/materialization_schedule_surface.spec.ts +0 -256
- package/src/service/materialization_scheduler.spec.ts +0 -289
- package/src/service/materialization_scheduler.ts +0 -304
- package/src/service/materialization_serve_transform.spec.ts +0 -1003
- package/src/service/materialization_serve_transform.ts +0 -779
- package/src/service/materialization_service.spec.ts +0 -2373
- package/src/service/materialization_service.ts +0 -2342
- package/src/service/materialization_test_fixtures.ts +0 -249
- package/src/service/model.spec.ts +0 -1157
- package/src/service/model.ts +0 -3370
- package/src/service/model_limits.spec.ts +0 -209
- package/src/service/model_limits.ts +0 -131
- package/src/service/model_storage_serve.spec.ts +0 -193
- package/src/service/model_storage_serve_joins.spec.ts +0 -193
- package/src/service/package.spec.ts +0 -612
- package/src/service/package.ts +0 -1799
- package/src/service/package_manifest.spec.ts +0 -230
- package/src/service/package_manifest.ts +0 -241
- package/src/service/package_memory_governor.spec.ts +0 -173
- package/src/service/package_memory_governor.ts +0 -233
- package/src/service/package_quote_bound_tables.spec.ts +0 -171
- package/src/service/package_race.spec.ts +0 -208
- package/src/service/package_reload_safety.spec.ts +0 -193
- package/src/service/package_rollback.spec.ts +0 -190
- package/src/service/package_worker_path.spec.ts +0 -645
- package/src/service/path_injection.spec.ts +0 -39
- package/src/service/persist_annotation_validation.spec.ts +0 -89
- package/src/service/persist_annotation_validation.ts +0 -59
- package/src/service/persistence_policy.spec.ts +0 -374
- package/src/service/proxy.spec.ts +0 -414
- package/src/service/proxy.ts +0 -248
- package/src/service/query_boundary.spec.ts +0 -470
- package/src/service/query_metadata.spec.ts +0 -408
- package/src/service/query_metadata.ts +0 -492
- package/src/service/query_metadata_identity.spec.ts +0 -149
- package/src/service/query_text.spec.ts +0 -79
- package/src/service/query_text.ts +0 -43
- package/src/service/quoting.spec.ts +0 -135
- package/src/service/quoting.ts +0 -78
- package/src/service/resolve_environment.ts +0 -15
- package/src/service/restricted_mode.spec.ts +0 -299
- package/src/service/source_extraction.ts +0 -225
- package/src/service/theme_store.ts +0 -199
- package/src/storage/DatabaseInterface.ts +0 -198
- package/src/storage/StorageManager.ts +0 -113
- package/src/storage/duckdb/ConnectionRepository.ts +0 -143
- package/src/storage/duckdb/DuckDBConnection.ts +0 -123
- package/src/storage/duckdb/DuckDBRepository.ts +0 -208
- package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
- package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
- package/src/storage/duckdb/MaterializationRepository.ts +0 -336
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/schema.ts +0 -229
- package/src/stream_helpers.spec.ts +0 -280
- package/src/stream_helpers.ts +0 -162
- package/src/telemetry.ts +0 -20
- package/src/test_helpers/metrics_harness.ts +0 -166
- package/src/theme_key_parity.spec.ts +0 -57
- package/src/utils.ts +0 -29
- package/tests/fixtures/authorize-compile/model.malloy +0 -9
- package/tests/fixtures/authorize-compile/publisher.json +0 -4
- package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
- package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/data.csv +0 -3
- package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
- package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
- package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
- package/tests/fixtures/html-pages-test/public/data.json +0 -1
- package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
- package/tests/fixtures/html-pages-test/public/index.html +0 -9
- package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
- package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
- package/tests/fixtures/html-pages-test/public/slides.html +0 -12
- package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
- package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
- package/tests/fixtures/html-pages-test/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/report.malloy +0 -1
- package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
- package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
- package/tests/fixtures/persist-multi-level/publisher.json +0 -5
- package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
- package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/fixtures/query-givens/data/orders.csv +0 -7
- package/tests/fixtures/query-givens/model.malloy +0 -34
- package/tests/fixtures/query-givens/publisher.json +0 -5
- package/tests/fixtures/xlsx/database.xlsx +0 -0
- package/tests/harness/mcp_test_setup.ts +0 -332
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
- package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
- package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
- package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
- package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
- package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
- package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
- package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
- package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
- package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
- package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
- package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
- package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
- package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
- package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
- package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
- package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
- package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
- package/tests/unit/controller/theme.controller.spec.ts +0 -141
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
- package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
- package/tests/unit/duckdb/repositories.test.ts +0 -208
- package/tests/unit/ducklake/ducklake.test.ts +0 -952
- package/tests/unit/service/theme_store.spec.ts +0 -139
- package/tsconfig.json +0 -32
|
@@ -1,1157 +0,0 @@
|
|
|
1
|
-
import { API, MalloyError, Runtime } from "@malloydata/malloy";
|
|
2
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import sinon from "sinon";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
AccessDeniedError,
|
|
8
|
-
BadRequestError,
|
|
9
|
-
ModelNotFoundError,
|
|
10
|
-
PayloadTooLargeError,
|
|
11
|
-
} from "../errors";
|
|
12
|
-
import { Model, ModelType } from "./model";
|
|
13
|
-
|
|
14
|
-
describe("service/model", () => {
|
|
15
|
-
const packageName = "test-package";
|
|
16
|
-
const mockPackageName = "mockPackage";
|
|
17
|
-
const mockPackagePath = "mockPackagePath";
|
|
18
|
-
const mockModelPath = "mockModel.malloy";
|
|
19
|
-
|
|
20
|
-
it("should create a Model instance", async () => {
|
|
21
|
-
sinon.stub(Model, "getModelRuntime").resolves({
|
|
22
|
-
runtime: sinon.createStubInstance(Runtime),
|
|
23
|
-
modelURL: new URL("file://mockModelPath"),
|
|
24
|
-
importBaseURL: new URL("file://mockBaseURL/"),
|
|
25
|
-
dataStyles: {},
|
|
26
|
-
modelType: "model",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
sinon.stub(Model, "getModelMaterializer").resolves({
|
|
30
|
-
modelMaterializer: undefined,
|
|
31
|
-
runnableNotebookCells: undefined,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const model = await Model.create(
|
|
35
|
-
mockPackageName,
|
|
36
|
-
mockPackagePath,
|
|
37
|
-
mockModelPath,
|
|
38
|
-
new Map(),
|
|
39
|
-
);
|
|
40
|
-
expect(model).toBeInstanceOf(Model);
|
|
41
|
-
expect(model.getPath()).toBe(mockModelPath);
|
|
42
|
-
|
|
43
|
-
sinon.restore();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("should handle ModelNotFoundError correctly", async () => {
|
|
47
|
-
await expect(async () => {
|
|
48
|
-
await Model.create(
|
|
49
|
-
mockPackageName,
|
|
50
|
-
mockPackagePath,
|
|
51
|
-
mockModelPath,
|
|
52
|
-
new Map(),
|
|
53
|
-
);
|
|
54
|
-
}).toThrowError(`${mockModelPath} does not exist.`);
|
|
55
|
-
|
|
56
|
-
sinon.restore();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("instance methods", () => {
|
|
60
|
-
describe("getPath", () => {
|
|
61
|
-
it("should return the correct modelPath", async () => {
|
|
62
|
-
const model = new Model(
|
|
63
|
-
packageName,
|
|
64
|
-
mockModelPath,
|
|
65
|
-
{},
|
|
66
|
-
"model",
|
|
67
|
-
undefined,
|
|
68
|
-
undefined,
|
|
69
|
-
undefined,
|
|
70
|
-
undefined,
|
|
71
|
-
undefined,
|
|
72
|
-
undefined,
|
|
73
|
-
undefined,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
expect(model.getPath()).toBe(mockModelPath);
|
|
77
|
-
|
|
78
|
-
sinon.restore();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("getType", () => {
|
|
83
|
-
it("should return the correct modelType", async () => {
|
|
84
|
-
const modelType = "model";
|
|
85
|
-
const model = new Model(
|
|
86
|
-
packageName,
|
|
87
|
-
mockModelPath,
|
|
88
|
-
{},
|
|
89
|
-
modelType,
|
|
90
|
-
undefined,
|
|
91
|
-
undefined,
|
|
92
|
-
undefined,
|
|
93
|
-
undefined,
|
|
94
|
-
undefined,
|
|
95
|
-
undefined,
|
|
96
|
-
undefined,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
expect(model.getType()).toBe(modelType);
|
|
100
|
-
|
|
101
|
-
sinon.restore();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("getModel", () => {
|
|
106
|
-
it("should throw ModelCompilationError if a compilation error exists", async () => {
|
|
107
|
-
const model = new Model(
|
|
108
|
-
packageName,
|
|
109
|
-
mockModelPath,
|
|
110
|
-
{},
|
|
111
|
-
"model",
|
|
112
|
-
undefined,
|
|
113
|
-
undefined,
|
|
114
|
-
undefined,
|
|
115
|
-
undefined,
|
|
116
|
-
undefined,
|
|
117
|
-
undefined,
|
|
118
|
-
new MalloyError("Compilation error"),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
await expect(async () => {
|
|
122
|
-
await model.getModel();
|
|
123
|
-
}).toThrowError(MalloyError);
|
|
124
|
-
|
|
125
|
-
sinon.restore();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("should throw ModelNotFoundError for invalid modelType", async () => {
|
|
129
|
-
const model = new Model(
|
|
130
|
-
packageName,
|
|
131
|
-
mockModelPath,
|
|
132
|
-
{},
|
|
133
|
-
"notebook" as ModelType,
|
|
134
|
-
undefined,
|
|
135
|
-
undefined,
|
|
136
|
-
undefined,
|
|
137
|
-
undefined,
|
|
138
|
-
undefined,
|
|
139
|
-
undefined,
|
|
140
|
-
undefined,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
await expect(async () => {
|
|
144
|
-
await model.getModel();
|
|
145
|
-
}).toThrowError(ModelNotFoundError);
|
|
146
|
-
|
|
147
|
-
sinon.restore();
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("getNotebook", () => {
|
|
152
|
-
it("should throw ModelCompilationError if a compilation error exists", async () => {
|
|
153
|
-
const model = new Model(
|
|
154
|
-
packageName,
|
|
155
|
-
mockModelPath,
|
|
156
|
-
{},
|
|
157
|
-
"notebook",
|
|
158
|
-
undefined,
|
|
159
|
-
undefined,
|
|
160
|
-
undefined,
|
|
161
|
-
undefined,
|
|
162
|
-
undefined,
|
|
163
|
-
undefined,
|
|
164
|
-
new Error("Compilation error"),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
await expect(async () => {
|
|
168
|
-
await model.getNotebook();
|
|
169
|
-
}).toThrowError(Error);
|
|
170
|
-
|
|
171
|
-
sinon.restore();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("should throw ModelNotFoundError for invalid modelType", async () => {
|
|
175
|
-
const model = new Model(
|
|
176
|
-
packageName,
|
|
177
|
-
mockModelPath,
|
|
178
|
-
{},
|
|
179
|
-
"model" as ModelType,
|
|
180
|
-
undefined,
|
|
181
|
-
undefined,
|
|
182
|
-
undefined,
|
|
183
|
-
undefined,
|
|
184
|
-
undefined,
|
|
185
|
-
undefined,
|
|
186
|
-
undefined,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
await expect(async () => {
|
|
190
|
-
await model.getNotebook();
|
|
191
|
-
}).toThrowError(ModelNotFoundError);
|
|
192
|
-
|
|
193
|
-
sinon.restore();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("embeds model-level givens in each newSources SourceInfo", async () => {
|
|
197
|
-
const sourceInfo = {
|
|
198
|
-
name: "carriers",
|
|
199
|
-
schema: { fields: [] },
|
|
200
|
-
};
|
|
201
|
-
const givens = [
|
|
202
|
-
{
|
|
203
|
-
name: "region",
|
|
204
|
-
type: "string",
|
|
205
|
-
annotations: ["#(doc) Region"],
|
|
206
|
-
},
|
|
207
|
-
];
|
|
208
|
-
const model = new Model(
|
|
209
|
-
packageName,
|
|
210
|
-
"test.malloynb",
|
|
211
|
-
{},
|
|
212
|
-
"notebook",
|
|
213
|
-
undefined, // modelMaterializer
|
|
214
|
-
undefined, // modelDef
|
|
215
|
-
undefined, // sources
|
|
216
|
-
undefined, // queries
|
|
217
|
-
undefined, // sourceInfos
|
|
218
|
-
[
|
|
219
|
-
{
|
|
220
|
-
type: "code",
|
|
221
|
-
text: "import 'carriers.malloy'",
|
|
222
|
-
newSources: [sourceInfo],
|
|
223
|
-
},
|
|
224
|
-
], // runnableNotebookCells
|
|
225
|
-
undefined, // compilationError
|
|
226
|
-
undefined, // filterMap
|
|
227
|
-
givens, // givens
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
const notebook = await model.getNotebook();
|
|
231
|
-
expect(notebook.notebookCells).toHaveLength(1);
|
|
232
|
-
const parsed = JSON.parse(
|
|
233
|
-
notebook.notebookCells![0].newSources![0],
|
|
234
|
-
);
|
|
235
|
-
expect(parsed.name).toBe("carriers");
|
|
236
|
-
// SourceInfo fields are preserved untouched.
|
|
237
|
-
expect(parsed.schema).toEqual({ fields: [] });
|
|
238
|
-
// Givens ride along verbatim — no second getModel round-trip needed.
|
|
239
|
-
expect(parsed.givens).toEqual(givens);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("omits givens from newSources when the model declares none", async () => {
|
|
243
|
-
const sourceInfo = { name: "carriers", schema: { fields: [] } };
|
|
244
|
-
const model = new Model(
|
|
245
|
-
packageName,
|
|
246
|
-
"test.malloynb",
|
|
247
|
-
{},
|
|
248
|
-
"notebook",
|
|
249
|
-
undefined,
|
|
250
|
-
undefined,
|
|
251
|
-
undefined,
|
|
252
|
-
undefined,
|
|
253
|
-
undefined,
|
|
254
|
-
[
|
|
255
|
-
{
|
|
256
|
-
type: "code",
|
|
257
|
-
text: "import 'carriers.malloy'",
|
|
258
|
-
newSources: [sourceInfo],
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
undefined,
|
|
262
|
-
undefined,
|
|
263
|
-
undefined, // no givens
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
const notebook = await model.getNotebook();
|
|
267
|
-
const parsed = JSON.parse(
|
|
268
|
-
notebook.notebookCells![0].newSources![0],
|
|
269
|
-
);
|
|
270
|
-
expect(parsed.name).toBe("carriers");
|
|
271
|
-
expect(parsed).not.toHaveProperty("givens");
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
describe("getQueryResults", () => {
|
|
276
|
-
it("should throw BadRequestError if a non-MalloyError compilation error exists", async () => {
|
|
277
|
-
const error = new Error("Compilation error");
|
|
278
|
-
const model = new Model(
|
|
279
|
-
packageName,
|
|
280
|
-
mockModelPath,
|
|
281
|
-
{},
|
|
282
|
-
"model",
|
|
283
|
-
undefined,
|
|
284
|
-
undefined,
|
|
285
|
-
undefined,
|
|
286
|
-
undefined,
|
|
287
|
-
undefined,
|
|
288
|
-
undefined,
|
|
289
|
-
error,
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
await expect(async () => {
|
|
293
|
-
await model.getQueryResults();
|
|
294
|
-
}).toThrowError(BadRequestError);
|
|
295
|
-
|
|
296
|
-
sinon.restore();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("should throw BadRequestError if no queryable entities exist", async () => {
|
|
300
|
-
const model = new Model(
|
|
301
|
-
packageName,
|
|
302
|
-
mockModelPath,
|
|
303
|
-
{},
|
|
304
|
-
"model",
|
|
305
|
-
undefined,
|
|
306
|
-
undefined,
|
|
307
|
-
undefined,
|
|
308
|
-
undefined,
|
|
309
|
-
undefined,
|
|
310
|
-
undefined,
|
|
311
|
-
undefined,
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
await expect(async () => {
|
|
315
|
-
await model.getQueryResults();
|
|
316
|
-
}).toThrowError(BadRequestError);
|
|
317
|
-
|
|
318
|
-
sinon.restore();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Both caller-driven compile paths — the free-form `query` text and the
|
|
322
|
-
// `run: source->view` string built from `sourceName`/`queryName` — must
|
|
323
|
-
// go through restricted mode. The trusted `loadQuery` is reserved for
|
|
324
|
-
// author-curated content (notebook cells) and must never be reached from
|
|
325
|
-
// `getQueryResults`. These tests pin the dispatch so a regression that
|
|
326
|
-
// re-routes either path back to `loadQuery` is caught.
|
|
327
|
-
describe("compile dispatch", () => {
|
|
328
|
-
function buildDispatchModel(): {
|
|
329
|
-
model: Model;
|
|
330
|
-
loadQuery: sinon.SinonStub;
|
|
331
|
-
loadRestrictedQuery: sinon.SinonStub;
|
|
332
|
-
} {
|
|
333
|
-
// getPreparedResult rejects so execution stops right after the
|
|
334
|
-
// loader call; we only assert which loader was invoked.
|
|
335
|
-
const runnableStub = {
|
|
336
|
-
getPreparedResult: sinon
|
|
337
|
-
.stub()
|
|
338
|
-
.rejects(new MalloyError("stub-stop", [])),
|
|
339
|
-
run: sinon.stub().rejects(new MalloyError("stub-stop", [])),
|
|
340
|
-
};
|
|
341
|
-
const loadQuery = sinon.stub().returns(runnableStub);
|
|
342
|
-
const loadRestrictedQuery = sinon.stub().returns(runnableStub);
|
|
343
|
-
const modelMaterializer = { loadQuery, loadRestrictedQuery };
|
|
344
|
-
const model = new Model(
|
|
345
|
-
packageName,
|
|
346
|
-
mockModelPath,
|
|
347
|
-
{},
|
|
348
|
-
"model",
|
|
349
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
350
|
-
modelMaterializer as any,
|
|
351
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
352
|
-
{ contents: {}, exports: [], queryList: [] } as any,
|
|
353
|
-
undefined,
|
|
354
|
-
undefined,
|
|
355
|
-
undefined,
|
|
356
|
-
undefined,
|
|
357
|
-
undefined,
|
|
358
|
-
);
|
|
359
|
-
return { model, loadQuery, loadRestrictedQuery };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
afterEach(() => sinon.restore());
|
|
363
|
-
|
|
364
|
-
it("compiles ad-hoc query text in restricted mode, never trusted loadQuery", async () => {
|
|
365
|
-
const { model, loadQuery, loadRestrictedQuery } =
|
|
366
|
-
buildDispatchModel();
|
|
367
|
-
|
|
368
|
-
await expect(
|
|
369
|
-
model.getQueryResults(
|
|
370
|
-
undefined,
|
|
371
|
-
undefined,
|
|
372
|
-
"run: orders -> { aggregate: c is count() }",
|
|
373
|
-
),
|
|
374
|
-
).rejects.toThrow(MalloyError);
|
|
375
|
-
|
|
376
|
-
expect(loadRestrictedQuery.calledOnce).toBe(true);
|
|
377
|
-
expect(loadQuery.called).toBe(false);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("compiles the named source/view path in restricted mode, never trusted loadQuery", async () => {
|
|
381
|
-
const { model, loadQuery, loadRestrictedQuery } =
|
|
382
|
-
buildDispatchModel();
|
|
383
|
-
|
|
384
|
-
await expect(
|
|
385
|
-
model.getQueryResults("orders", "summary"),
|
|
386
|
-
).rejects.toThrow(MalloyError);
|
|
387
|
-
|
|
388
|
-
expect(loadRestrictedQuery.calledOnce).toBe(true);
|
|
389
|
-
expect(loadQuery.called).toBe(false);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("forwards givens to runnable.getPreparedResult and .run", async () => {
|
|
394
|
-
const givensArg = { region: "EU" };
|
|
395
|
-
const preparedResultStub = sinon
|
|
396
|
-
.stub()
|
|
397
|
-
.resolves({ resultExplore: { limit: 10 } });
|
|
398
|
-
const runStub = sinon
|
|
399
|
-
.stub()
|
|
400
|
-
.rejects(new MalloyError("stub-stop", []));
|
|
401
|
-
const runnableStub = {
|
|
402
|
-
getPreparedResult: preparedResultStub,
|
|
403
|
-
run: runStub,
|
|
404
|
-
};
|
|
405
|
-
const modelMaterializer = {
|
|
406
|
-
loadQuery: sinon.stub().returns(runnableStub),
|
|
407
|
-
loadRestrictedQuery: sinon.stub().returns(runnableStub),
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const model = new Model(
|
|
411
|
-
packageName,
|
|
412
|
-
mockModelPath,
|
|
413
|
-
{},
|
|
414
|
-
"model",
|
|
415
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
416
|
-
modelMaterializer as any,
|
|
417
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
418
|
-
{ contents: {}, exports: [], queryList: [] } as any,
|
|
419
|
-
undefined,
|
|
420
|
-
undefined,
|
|
421
|
-
undefined,
|
|
422
|
-
undefined,
|
|
423
|
-
undefined,
|
|
424
|
-
undefined,
|
|
425
|
-
// Model surfaces `region` so filterGivensToModelSurface (see
|
|
426
|
-
// model.ts) forwards it rather than dropping it as unknown.
|
|
427
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
428
|
-
[{ name: "region", type: "string" }] as any,
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
await expect(
|
|
432
|
-
model.getQueryResults(
|
|
433
|
-
undefined,
|
|
434
|
-
undefined,
|
|
435
|
-
"run: orders -> summary",
|
|
436
|
-
undefined,
|
|
437
|
-
undefined,
|
|
438
|
-
givensArg,
|
|
439
|
-
),
|
|
440
|
-
).rejects.toThrow(MalloyError);
|
|
441
|
-
|
|
442
|
-
expect(preparedResultStub.calledOnce).toBe(true);
|
|
443
|
-
expect(preparedResultStub.firstCall.args[0]).toEqual({
|
|
444
|
-
givens: givensArg,
|
|
445
|
-
});
|
|
446
|
-
expect(runStub.firstCall.args[0]).toMatchObject({
|
|
447
|
-
givens: givensArg,
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
sinon.restore();
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("maps a finalized-given rejection (code) to BadRequestError, not 500", async () => {
|
|
454
|
-
// Malloy throws this (extends Error, not MalloyError, not root-exported)
|
|
455
|
-
// when a client supplies a given an operator finalized. model.ts
|
|
456
|
-
// duck-types on `.code`; guard against that mapping regressing.
|
|
457
|
-
const finalizedErr = Object.assign(
|
|
458
|
-
new Error(
|
|
459
|
-
"Given 'region' is finalized and cannot be overridden",
|
|
460
|
-
),
|
|
461
|
-
{ code: "runtime-given-finalized" },
|
|
462
|
-
);
|
|
463
|
-
const runnableStub = {
|
|
464
|
-
getPreparedResult: sinon.stub().rejects(finalizedErr),
|
|
465
|
-
run: sinon.stub(),
|
|
466
|
-
};
|
|
467
|
-
const modelMaterializer = {
|
|
468
|
-
loadQuery: sinon.stub().returns(runnableStub),
|
|
469
|
-
loadRestrictedQuery: sinon.stub().returns(runnableStub),
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
const model = new Model(
|
|
473
|
-
packageName,
|
|
474
|
-
mockModelPath,
|
|
475
|
-
{},
|
|
476
|
-
"model",
|
|
477
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
478
|
-
modelMaterializer as any,
|
|
479
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
480
|
-
{ contents: {}, exports: [], queryList: [] } as any,
|
|
481
|
-
undefined,
|
|
482
|
-
undefined,
|
|
483
|
-
undefined,
|
|
484
|
-
undefined,
|
|
485
|
-
undefined,
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
await expect(
|
|
489
|
-
model.getQueryResults(
|
|
490
|
-
undefined,
|
|
491
|
-
undefined,
|
|
492
|
-
"run: orders -> summary",
|
|
493
|
-
undefined,
|
|
494
|
-
undefined,
|
|
495
|
-
{ region: "EU" },
|
|
496
|
-
),
|
|
497
|
-
).rejects.toThrow(BadRequestError);
|
|
498
|
-
|
|
499
|
-
sinon.restore();
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* The row/byte caps live in `model_limits.ts` (unit-tested in
|
|
504
|
-
* `model_limits.spec.ts`); these tests just confirm the wiring —
|
|
505
|
-
* that `Model.getQueryResults` calls the helpers with the right
|
|
506
|
-
* values and that an overflow propagates as `PayloadTooLargeError`
|
|
507
|
-
* (HTTP 413), not the generic `BadRequestError` (HTTP 400).
|
|
508
|
-
*/
|
|
509
|
-
describe("response caps", () => {
|
|
510
|
-
const originalRowsEnv = process.env.PUBLISHER_MAX_QUERY_ROWS;
|
|
511
|
-
const originalBytesEnv = process.env.PUBLISHER_MAX_RESPONSE_BYTES;
|
|
512
|
-
const originalDefaultEnv =
|
|
513
|
-
process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT;
|
|
514
|
-
|
|
515
|
-
afterEach(() => {
|
|
516
|
-
sinon.restore();
|
|
517
|
-
for (const [name, original] of [
|
|
518
|
-
["PUBLISHER_MAX_QUERY_ROWS", originalRowsEnv],
|
|
519
|
-
["PUBLISHER_MAX_RESPONSE_BYTES", originalBytesEnv],
|
|
520
|
-
["PUBLISHER_DEFAULT_QUERY_ROW_LIMIT", originalDefaultEnv],
|
|
521
|
-
] as const) {
|
|
522
|
-
if (original === undefined) {
|
|
523
|
-
delete process.env[name];
|
|
524
|
-
} else {
|
|
525
|
-
process.env[name] = original;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Build a Model whose `runnable.run` resolves to a fake Result
|
|
532
|
-
* with the given totalRows; stub `API.util.wrapResult` so we
|
|
533
|
-
* don't need to construct a real Malloy schema/queryResult.
|
|
534
|
-
*/
|
|
535
|
-
function buildModelWithFakeRun(opts: {
|
|
536
|
-
userLimit?: number;
|
|
537
|
-
totalRows: number;
|
|
538
|
-
wrappedJson: object;
|
|
539
|
-
}): { model: Model; runStub: sinon.SinonStub } {
|
|
540
|
-
const preparedResultStub = sinon
|
|
541
|
-
.stub()
|
|
542
|
-
.resolves({ resultExplore: { limit: opts.userLimit ?? 0 } });
|
|
543
|
-
const fakeResult = {
|
|
544
|
-
_queryResult: { data: { rawData: [] } },
|
|
545
|
-
totalRows: opts.totalRows,
|
|
546
|
-
data: { value: [] },
|
|
547
|
-
connectionName: "fake",
|
|
548
|
-
};
|
|
549
|
-
const runStub = sinon.stub().resolves(fakeResult);
|
|
550
|
-
sinon
|
|
551
|
-
.stub(API.util, "wrapResult")
|
|
552
|
-
.returns(
|
|
553
|
-
opts.wrappedJson as unknown as ReturnType<
|
|
554
|
-
typeof API.util.wrapResult
|
|
555
|
-
>,
|
|
556
|
-
);
|
|
557
|
-
const runnableStub = {
|
|
558
|
-
getPreparedResult: preparedResultStub,
|
|
559
|
-
run: runStub,
|
|
560
|
-
};
|
|
561
|
-
const modelMaterializer = {
|
|
562
|
-
loadQuery: sinon.stub().returns(runnableStub),
|
|
563
|
-
loadRestrictedQuery: sinon.stub().returns(runnableStub),
|
|
564
|
-
};
|
|
565
|
-
const model = new Model(
|
|
566
|
-
packageName,
|
|
567
|
-
mockModelPath,
|
|
568
|
-
{},
|
|
569
|
-
"model",
|
|
570
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
571
|
-
modelMaterializer as any,
|
|
572
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
573
|
-
{ contents: {}, exports: [], queryList: [] } as any,
|
|
574
|
-
undefined,
|
|
575
|
-
undefined,
|
|
576
|
-
undefined,
|
|
577
|
-
undefined,
|
|
578
|
-
undefined,
|
|
579
|
-
);
|
|
580
|
-
return { model, runStub };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
it("clamps user LIMIT to maxRows + 1 when the user requested more than the cap", async () => {
|
|
584
|
-
process.env.PUBLISHER_MAX_QUERY_ROWS = "100";
|
|
585
|
-
const { model, runStub } = buildModelWithFakeRun({
|
|
586
|
-
userLimit: 1_000_000,
|
|
587
|
-
totalRows: 10,
|
|
588
|
-
wrappedJson: { rows: [] },
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
await model.getQueryResults(
|
|
592
|
-
undefined,
|
|
593
|
-
undefined,
|
|
594
|
-
"run: orders -> summary",
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
expect(runStub.firstCall.args[0].rowLimit).toBe(101);
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
it("passes user LIMIT through when below maxRows", async () => {
|
|
601
|
-
process.env.PUBLISHER_MAX_QUERY_ROWS = "100";
|
|
602
|
-
const { model, runStub } = buildModelWithFakeRun({
|
|
603
|
-
userLimit: 50,
|
|
604
|
-
totalRows: 10,
|
|
605
|
-
wrappedJson: { rows: [] },
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
await model.getQueryResults(
|
|
609
|
-
undefined,
|
|
610
|
-
undefined,
|
|
611
|
-
"run: orders -> summary",
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
expect(runStub.firstCall.args[0].rowLimit).toBe(50);
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it("falls back to PUBLISHER_DEFAULT_QUERY_ROW_LIMIT when the user query has no LIMIT", async () => {
|
|
618
|
-
process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "42";
|
|
619
|
-
delete process.env.PUBLISHER_MAX_QUERY_ROWS;
|
|
620
|
-
const { model, runStub } = buildModelWithFakeRun({
|
|
621
|
-
userLimit: 0,
|
|
622
|
-
totalRows: 10,
|
|
623
|
-
wrappedJson: { rows: [] },
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
await model.getQueryResults(
|
|
627
|
-
undefined,
|
|
628
|
-
undefined,
|
|
629
|
-
"run: orders -> summary",
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
expect(runStub.firstCall.args[0].rowLimit).toBe(42);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it("throws PayloadTooLargeError (not BadRequestError) when totalRows exceeds the cap", async () => {
|
|
636
|
-
process.env.PUBLISHER_MAX_QUERY_ROWS = "100";
|
|
637
|
-
const { model } = buildModelWithFakeRun({
|
|
638
|
-
userLimit: 1000,
|
|
639
|
-
totalRows: 101,
|
|
640
|
-
wrappedJson: { rows: [] },
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
await expect(
|
|
644
|
-
model.getQueryResults(
|
|
645
|
-
undefined,
|
|
646
|
-
undefined,
|
|
647
|
-
"run: orders -> summary",
|
|
648
|
-
),
|
|
649
|
-
).rejects.toBeInstanceOf(PayloadTooLargeError);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
it("throws PayloadTooLargeError when the wrapped response exceeds the byte cap", async () => {
|
|
653
|
-
process.env.PUBLISHER_MAX_QUERY_ROWS = "1000";
|
|
654
|
-
process.env.PUBLISHER_MAX_RESPONSE_BYTES = "100";
|
|
655
|
-
const huge = "x".repeat(500);
|
|
656
|
-
const { model } = buildModelWithFakeRun({
|
|
657
|
-
userLimit: 10,
|
|
658
|
-
totalRows: 10,
|
|
659
|
-
wrappedJson: { rows: [{ s: huge }] },
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
await expect(
|
|
663
|
-
model.getQueryResults(
|
|
664
|
-
undefined,
|
|
665
|
-
undefined,
|
|
666
|
-
"run: orders -> summary",
|
|
667
|
-
),
|
|
668
|
-
).rejects.toBeInstanceOf(PayloadTooLargeError);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
it("does not throw when both counts are within their caps", async () => {
|
|
672
|
-
process.env.PUBLISHER_MAX_QUERY_ROWS = "1000";
|
|
673
|
-
process.env.PUBLISHER_MAX_RESPONSE_BYTES = "10000";
|
|
674
|
-
const { model } = buildModelWithFakeRun({
|
|
675
|
-
userLimit: 10,
|
|
676
|
-
totalRows: 10,
|
|
677
|
-
wrappedJson: { rows: [{ a: 1 }] },
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
await expect(
|
|
681
|
-
model.getQueryResults(
|
|
682
|
-
undefined,
|
|
683
|
-
undefined,
|
|
684
|
-
"run: orders -> summary",
|
|
685
|
-
),
|
|
686
|
-
).resolves.toBeDefined();
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
describe("executeNotebookCell", () => {
|
|
692
|
-
it("forwards givens to runnable.getPreparedResult and .run", async () => {
|
|
693
|
-
const givensArg = { target_code: "AA" };
|
|
694
|
-
const preparedResultStub = sinon
|
|
695
|
-
.stub()
|
|
696
|
-
.resolves({ resultExplore: { limit: 10 } });
|
|
697
|
-
const runStub = sinon
|
|
698
|
-
.stub()
|
|
699
|
-
.rejects(new MalloyError("stub-stop", []));
|
|
700
|
-
const cellRunnable = {
|
|
701
|
-
getPreparedResult: preparedResultStub,
|
|
702
|
-
run: runStub,
|
|
703
|
-
};
|
|
704
|
-
const runnableCells = [
|
|
705
|
-
{
|
|
706
|
-
type: "code" as const,
|
|
707
|
-
text: "run: orders -> by_code",
|
|
708
|
-
runnable: cellRunnable,
|
|
709
|
-
},
|
|
710
|
-
];
|
|
711
|
-
|
|
712
|
-
const model = new Model(
|
|
713
|
-
packageName,
|
|
714
|
-
"test.malloynb",
|
|
715
|
-
{},
|
|
716
|
-
"notebook",
|
|
717
|
-
undefined,
|
|
718
|
-
undefined,
|
|
719
|
-
undefined,
|
|
720
|
-
undefined,
|
|
721
|
-
undefined,
|
|
722
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
723
|
-
runnableCells as any,
|
|
724
|
-
undefined,
|
|
725
|
-
undefined,
|
|
726
|
-
// Model surfaces `target_code` so filterGivensToModelSurface
|
|
727
|
-
// (see model.ts) forwards it rather than dropping it as unknown.
|
|
728
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
729
|
-
[{ name: "target_code", type: "string" }] as any,
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
await expect(
|
|
733
|
-
model.executeNotebookCell(0, undefined, undefined, givensArg),
|
|
734
|
-
).rejects.toThrow(MalloyError);
|
|
735
|
-
|
|
736
|
-
expect(preparedResultStub.calledOnce).toBe(true);
|
|
737
|
-
expect(preparedResultStub.firstCall.args[0]).toEqual({
|
|
738
|
-
givens: givensArg,
|
|
739
|
-
});
|
|
740
|
-
expect(runStub.firstCall.args[0]).toMatchObject({
|
|
741
|
-
givens: givensArg,
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
sinon.restore();
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
it("maps a finalized-given rejection (code) to BadRequestError, not 500", async () => {
|
|
748
|
-
const finalizedErr = Object.assign(
|
|
749
|
-
new Error(
|
|
750
|
-
"Given 'target_code' is finalized and cannot be overridden",
|
|
751
|
-
),
|
|
752
|
-
{ code: "runtime-given-finalized" },
|
|
753
|
-
);
|
|
754
|
-
const cellRunnable = {
|
|
755
|
-
getPreparedResult: sinon.stub().rejects(finalizedErr),
|
|
756
|
-
run: sinon.stub(),
|
|
757
|
-
};
|
|
758
|
-
const runnableCells = [
|
|
759
|
-
{
|
|
760
|
-
type: "code" as const,
|
|
761
|
-
text: "run: orders -> by_code",
|
|
762
|
-
runnable: cellRunnable,
|
|
763
|
-
},
|
|
764
|
-
];
|
|
765
|
-
|
|
766
|
-
const model = new Model(
|
|
767
|
-
packageName,
|
|
768
|
-
"test.malloynb",
|
|
769
|
-
{},
|
|
770
|
-
"notebook",
|
|
771
|
-
undefined,
|
|
772
|
-
undefined,
|
|
773
|
-
undefined,
|
|
774
|
-
undefined,
|
|
775
|
-
undefined,
|
|
776
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
777
|
-
runnableCells as any,
|
|
778
|
-
undefined,
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
await expect(
|
|
782
|
-
model.executeNotebookCell(0, undefined, undefined, {
|
|
783
|
-
target_code: "AA",
|
|
784
|
-
}),
|
|
785
|
-
).rejects.toThrow(BadRequestError);
|
|
786
|
-
|
|
787
|
-
sinon.restore();
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
it("embeds model-level givens in executed cell newSources", async () => {
|
|
791
|
-
const sourceInfo = { name: "carriers", schema: { fields: [] } };
|
|
792
|
-
const givens = [
|
|
793
|
-
{
|
|
794
|
-
name: "region",
|
|
795
|
-
type: "string",
|
|
796
|
-
annotations: ["#(doc) Region"],
|
|
797
|
-
},
|
|
798
|
-
];
|
|
799
|
-
// A source-only code cell (no runnable) still emits newSources.
|
|
800
|
-
const runnableCells = [
|
|
801
|
-
{
|
|
802
|
-
type: "code" as const,
|
|
803
|
-
text: "import 'carriers.malloy'",
|
|
804
|
-
newSources: [sourceInfo],
|
|
805
|
-
},
|
|
806
|
-
];
|
|
807
|
-
|
|
808
|
-
const model = new Model(
|
|
809
|
-
packageName,
|
|
810
|
-
"test.malloynb",
|
|
811
|
-
{},
|
|
812
|
-
"notebook",
|
|
813
|
-
undefined, // modelMaterializer
|
|
814
|
-
undefined, // modelDef
|
|
815
|
-
undefined, // sources
|
|
816
|
-
undefined, // queries
|
|
817
|
-
undefined, // sourceInfos
|
|
818
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
819
|
-
runnableCells as any, // runnableNotebookCells
|
|
820
|
-
undefined, // compilationError
|
|
821
|
-
undefined, // filterMap
|
|
822
|
-
givens, // givens
|
|
823
|
-
);
|
|
824
|
-
|
|
825
|
-
const result = await model.executeNotebookCell(0);
|
|
826
|
-
const parsed = JSON.parse(result.newSources![0]);
|
|
827
|
-
expect(parsed.name).toBe("carriers");
|
|
828
|
-
expect(parsed.givens).toEqual(givens);
|
|
829
|
-
});
|
|
830
|
-
});
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
describe("authorize struct-resolution drift invariant", () => {
|
|
834
|
-
// collectAllReachableGates walks struct.fields using Malloy's own
|
|
835
|
-
// isJoined ('join' in sd) / isSourceDef (struct.type is a known source
|
|
836
|
-
// kind) guards. If a future Malloy version adds a new source `type`
|
|
837
|
-
// that isSourceDef doesn't yet recognize, a genuinely-joined field
|
|
838
|
-
// would fail isSourceDef and used to be silently `continue`d past —
|
|
839
|
-
// failing OPEN (the joined source's gate is never found, so it's never
|
|
840
|
-
// evaluated). This confirms the walk instead denies loudly when it
|
|
841
|
-
// finds a field that IS a join but can't be resolved to a walkable
|
|
842
|
-
// SourceDef, rather than silently treating it as "nothing to gate".
|
|
843
|
-
it("denies rather than silently skipping when a joined field doesn't resolve to a walkable SourceDef", async () => {
|
|
844
|
-
const driftedJoinField = {
|
|
845
|
-
name: "mystery_join",
|
|
846
|
-
join: "one", // isJoined: true ('join' in sd)
|
|
847
|
-
// A source `type` isSourceDef doesn't (yet) recognize -- stands
|
|
848
|
-
// in for a future Malloy source kind the duck-typed guard hasn't
|
|
849
|
-
// been updated to cover.
|
|
850
|
-
type: "future_source_kind",
|
|
851
|
-
fields: [],
|
|
852
|
-
};
|
|
853
|
-
const runStruct = {
|
|
854
|
-
type: "table",
|
|
855
|
-
name: "root",
|
|
856
|
-
fields: [driftedJoinField],
|
|
857
|
-
};
|
|
858
|
-
const modelDef = { contents: {}, exports: [], queryList: [] };
|
|
859
|
-
const runnable = {
|
|
860
|
-
getPreparedQuery: sinon.stub().resolves({
|
|
861
|
-
_query: { structRef: runStruct },
|
|
862
|
-
_modelDef: modelDef,
|
|
863
|
-
}),
|
|
864
|
-
};
|
|
865
|
-
|
|
866
|
-
const model = new Model(
|
|
867
|
-
packageName,
|
|
868
|
-
mockModelPath,
|
|
869
|
-
{},
|
|
870
|
-
"model",
|
|
871
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
872
|
-
{ loadQuery: sinon.stub() } as any, // modelMaterializer
|
|
873
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
874
|
-
modelDef as any,
|
|
875
|
-
undefined,
|
|
876
|
-
undefined,
|
|
877
|
-
undefined,
|
|
878
|
-
undefined,
|
|
879
|
-
undefined,
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
await expect(
|
|
883
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
884
|
-
model.assertAuthorizedForAllSources(runnable as any, {}),
|
|
885
|
-
).rejects.toThrow(AccessDeniedError);
|
|
886
|
-
});
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
// A binding that declares `freshnessFallback=live` says the tier is an
|
|
890
|
-
// optimisation, not a dependency: a store that fails under a routed query
|
|
891
|
-
// degrades to serving live. The failure modes here are all silent-by-nature —
|
|
892
|
-
// a wrong row cap returns an EMPTY success, a mislabelled metric inflates the
|
|
893
|
-
// tier's headline KPI while it is broken — so they need assertions rather than
|
|
894
|
-
// an end-to-end observation.
|
|
895
|
-
describe("runtime storage failure → freshnessFallback=live", () => {
|
|
896
|
-
const originalMode = process.env.PERSIST_STORAGE_MODE;
|
|
897
|
-
const originalDefaultRows = process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT;
|
|
898
|
-
const originalMetadata = process.env.PUBLISHER_QUERY_METADATA;
|
|
899
|
-
|
|
900
|
-
afterEach(() => {
|
|
901
|
-
sinon.restore();
|
|
902
|
-
for (const [name, original] of [
|
|
903
|
-
["PERSIST_STORAGE_MODE", originalMode],
|
|
904
|
-
["PUBLISHER_DEFAULT_QUERY_ROW_LIMIT", originalDefaultRows],
|
|
905
|
-
["PUBLISHER_QUERY_METADATA", originalMetadata],
|
|
906
|
-
] as const) {
|
|
907
|
-
if (original === undefined) delete process.env[name];
|
|
908
|
-
else process.env[name] = original;
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
const binding = (sourceName: string, freshnessFallback?: string) =>
|
|
913
|
-
({
|
|
914
|
-
sourceName,
|
|
915
|
-
connectionName: "lake",
|
|
916
|
-
virtualHandle: `eid-${sourceName}`,
|
|
917
|
-
tablePath: `lake.t_${sourceName}`,
|
|
918
|
-
schema: [{ name: "region", type: "string" }],
|
|
919
|
-
freshnessFallback,
|
|
920
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
921
|
-
}) as any;
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* A Model routed to storage, where the storage runnable fails and the live
|
|
925
|
-
* one succeeds. `loadServeShapeQuery` is stubbed because the real one
|
|
926
|
-
* compiles a transient model — the logic under test is the catch, and the
|
|
927
|
-
* SHAPE'S bindings (not the package's) are what it must consult.
|
|
928
|
-
*/
|
|
929
|
-
function routedModel(opts: {
|
|
930
|
-
shapeBindings: unknown[];
|
|
931
|
-
packageBindings?: unknown[];
|
|
932
|
-
storageFailsAt: "prepare" | "run";
|
|
933
|
-
liveRunFails?: boolean;
|
|
934
|
-
livePreparedLimit?: number;
|
|
935
|
-
}) {
|
|
936
|
-
const storageErr = new Error("store table missing");
|
|
937
|
-
const storageRunnable = {
|
|
938
|
-
getPreparedResult:
|
|
939
|
-
opts.storageFailsAt === "prepare"
|
|
940
|
-
? sinon.stub().rejects(storageErr)
|
|
941
|
-
: sinon.stub().resolves({
|
|
942
|
-
resultExplore: { limit: 0 },
|
|
943
|
-
connectionName: "lake",
|
|
944
|
-
}),
|
|
945
|
-
run:
|
|
946
|
-
opts.storageFailsAt === "run"
|
|
947
|
-
? sinon.stub().rejects(storageErr)
|
|
948
|
-
: sinon.stub().resolves({}),
|
|
949
|
-
};
|
|
950
|
-
const fakeResult = {
|
|
951
|
-
_queryResult: { data: { rawData: [] } },
|
|
952
|
-
totalRows: 1,
|
|
953
|
-
data: { value: [] },
|
|
954
|
-
connectionName: "live_pg",
|
|
955
|
-
};
|
|
956
|
-
const liveRun = opts.liveRunFails
|
|
957
|
-
? sinon.stub().rejects(new Error("warehouse down"))
|
|
958
|
-
: sinon.stub().resolves(fakeResult);
|
|
959
|
-
const liveRunnable = {
|
|
960
|
-
getPreparedResult: sinon.stub().resolves({
|
|
961
|
-
resultExplore: { limit: opts.livePreparedLimit ?? 0 },
|
|
962
|
-
connectionName: "live_pg",
|
|
963
|
-
}),
|
|
964
|
-
run: liveRun,
|
|
965
|
-
};
|
|
966
|
-
sinon
|
|
967
|
-
.stub(API.util, "wrapResult")
|
|
968
|
-
.returns({ rows: [] } as unknown as ReturnType<
|
|
969
|
-
typeof API.util.wrapResult
|
|
970
|
-
>);
|
|
971
|
-
const modelMaterializer = {
|
|
972
|
-
loadQuery: sinon.stub().returns(liveRunnable),
|
|
973
|
-
loadRestrictedQuery: sinon.stub().returns(liveRunnable),
|
|
974
|
-
};
|
|
975
|
-
const model = new Model(
|
|
976
|
-
packageName,
|
|
977
|
-
mockModelPath,
|
|
978
|
-
{},
|
|
979
|
-
"model",
|
|
980
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
981
|
-
modelMaterializer as any,
|
|
982
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
983
|
-
{ contents: {}, exports: [], queryList: [] } as any,
|
|
984
|
-
undefined,
|
|
985
|
-
undefined,
|
|
986
|
-
undefined,
|
|
987
|
-
undefined,
|
|
988
|
-
undefined,
|
|
989
|
-
);
|
|
990
|
-
process.env.PERSIST_STORAGE_MODE = "on";
|
|
991
|
-
model.setServeBindings(
|
|
992
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
993
|
-
(opts.packageBindings ?? opts.shapeBindings) as any,
|
|
994
|
-
);
|
|
995
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
996
|
-
model.setServeMalloyConfig({} as any);
|
|
997
|
-
sinon
|
|
998
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
999
|
-
.stub(model as any, "loadServeShapeQuery")
|
|
1000
|
-
.resolves({
|
|
1001
|
-
runnable: storageRunnable,
|
|
1002
|
-
virtualMap: { v: "lake.t_x" },
|
|
1003
|
-
bindings: opts.shapeBindings,
|
|
1004
|
-
});
|
|
1005
|
-
const recordStub = sinon.stub(
|
|
1006
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1007
|
-
(model as any).queryExecutionHistogram,
|
|
1008
|
-
"record",
|
|
1009
|
-
);
|
|
1010
|
-
return { model, liveRun, recordStub };
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
it("caps the live retry by the LIVE row limit when the store fails at prepare", async () => {
|
|
1014
|
-
// `rowLimit` is assigned from the storage prepare, so a prepare-time
|
|
1015
|
-
// failure leaves it 0 — which the connector reads as "stop before the
|
|
1016
|
-
// first row", returning a successful EMPTY answer instead of the data.
|
|
1017
|
-
process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "250";
|
|
1018
|
-
const { model, liveRun } = routedModel({
|
|
1019
|
-
shapeBindings: [binding("daily", "live")],
|
|
1020
|
-
storageFailsAt: "prepare",
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
await model.getQueryResults(undefined, undefined, "run: daily -> x");
|
|
1024
|
-
|
|
1025
|
-
expect(liveRun.calledOnce).toBe(true);
|
|
1026
|
-
expect(liveRun.firstCall.args[0].rowLimit).toBe(250);
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
it("tags the live retry, with the LIVE connection's layers and the same id", async () => {
|
|
1030
|
-
// Two failures in one: the statement that actually answered the query
|
|
1031
|
-
// carried no attribution at all, so the full-cost fallback — the one an
|
|
1032
|
-
// operator goes looking for on a bill — landed in the untagged bucket;
|
|
1033
|
-
// and the response still returned the id from the bag resolved before
|
|
1034
|
-
// the storage attempt, pointing a caller at the statement that FAILED.
|
|
1035
|
-
// The layers must come from the live connection: on this tier the store
|
|
1036
|
-
// is routinely a different connection, with different enforced
|
|
1037
|
-
// properties.
|
|
1038
|
-
process.env.PUBLISHER_QUERY_METADATA = "on";
|
|
1039
|
-
const { model, liveRun } = routedModel({
|
|
1040
|
-
shapeBindings: [binding("daily", "live")],
|
|
1041
|
-
storageFailsAt: "run",
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
const result = await model.getQueryResults(
|
|
1045
|
-
undefined,
|
|
1046
|
-
undefined,
|
|
1047
|
-
"run: daily -> x",
|
|
1048
|
-
undefined,
|
|
1049
|
-
undefined,
|
|
1050
|
-
undefined,
|
|
1051
|
-
undefined,
|
|
1052
|
-
{
|
|
1053
|
-
correlationId: "corr-1",
|
|
1054
|
-
connectionMetadata: (connectionName: string) =>
|
|
1055
|
-
connectionName === "live_pg"
|
|
1056
|
-
? { default: null, enforced: { tenant: "acme" } }
|
|
1057
|
-
: { default: null, enforced: { tenant: "wrong" } },
|
|
1058
|
-
},
|
|
1059
|
-
);
|
|
1060
|
-
|
|
1061
|
-
const attached = liveRun.firstCall.args[0].queryMetadata;
|
|
1062
|
-
expect(attached.tenant).toBe("acme");
|
|
1063
|
-
expect(attached.query_id).toBe("corr-1");
|
|
1064
|
-
expect(result.queryCorrelationId).toBe("corr-1");
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
it("does not record a live-served answer as a storage hit", async () => {
|
|
1068
|
-
// The hit rate is the tier's headline KPI; counting a fallback as a hit
|
|
1069
|
-
// makes it RISE while the tier is broken.
|
|
1070
|
-
const { model, recordStub } = routedModel({
|
|
1071
|
-
shapeBindings: [binding("daily", "live")],
|
|
1072
|
-
storageFailsAt: "run",
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
await model.getQueryResults(undefined, undefined, "run: daily -> x");
|
|
1076
|
-
|
|
1077
|
-
const success = recordStub
|
|
1078
|
-
.getCalls()
|
|
1079
|
-
.map((c) => c.args[1] as Record<string, string>)
|
|
1080
|
-
.find((a) => a["malloy.model.query.status"] === "success");
|
|
1081
|
-
expect(success?.["malloy.model.query.served_from"]).toBe(
|
|
1082
|
-
"live_fallback",
|
|
1083
|
-
);
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
it("maps a failure of the live retry like any other query failure", async () => {
|
|
1087
|
-
// A broad outage takes the warehouse down alongside the store, so the
|
|
1088
|
-
// retry failing is ordinary — and an unmapped throw turns a clean 400
|
|
1089
|
-
// into a 500 with no error metric behind it.
|
|
1090
|
-
const { model, recordStub } = routedModel({
|
|
1091
|
-
shapeBindings: [binding("daily", "live")],
|
|
1092
|
-
storageFailsAt: "run",
|
|
1093
|
-
liveRunFails: true,
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
|
-
await expect(
|
|
1097
|
-
model.getQueryResults(undefined, undefined, "run: daily -> x"),
|
|
1098
|
-
).rejects.toThrow(BadRequestError);
|
|
1099
|
-
|
|
1100
|
-
const errored = recordStub
|
|
1101
|
-
.getCalls()
|
|
1102
|
-
.map((c) => c.args[1] as Record<string, string>)
|
|
1103
|
-
.find((a) => a["malloy.model.query.status"] === "error");
|
|
1104
|
-
expect(errored).toBeDefined();
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
it("decides on the SHAPE's bindings, not the package's", async () => {
|
|
1108
|
-
// Bindings are pushed package-wide and `freshnessFallback` is per entry,
|
|
1109
|
-
// so a mixed set is normal. A query whose shape carries only the `live`
|
|
1110
|
-
// binding must still degrade — a `stale_ok` sibling it never touched
|
|
1111
|
-
// cannot veto it.
|
|
1112
|
-
const { model, liveRun } = routedModel({
|
|
1113
|
-
shapeBindings: [binding("daily", "live")],
|
|
1114
|
-
packageBindings: [
|
|
1115
|
-
binding("daily", "live"),
|
|
1116
|
-
binding("other", "stale_ok"),
|
|
1117
|
-
],
|
|
1118
|
-
storageFailsAt: "run",
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
await model.getQueryResults(undefined, undefined, "run: daily -> x");
|
|
1122
|
-
|
|
1123
|
-
expect(liveRun.calledOnce).toBe(true);
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
it("keeps surfacing the error when the shape carries a non-live binding", async () => {
|
|
1127
|
-
// `fail` and the `stale_ok` default are fail-closed: the caller asked to
|
|
1128
|
-
// hear about it rather than be served a slower answer.
|
|
1129
|
-
const { model } = routedModel({
|
|
1130
|
-
shapeBindings: [binding("daily", "stale_ok")],
|
|
1131
|
-
storageFailsAt: "run",
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
await expect(
|
|
1135
|
-
model.getQueryResults(undefined, undefined, "run: daily -> x"),
|
|
1136
|
-
).rejects.toThrow(BadRequestError);
|
|
1137
|
-
});
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
describe("static methods", () => {
|
|
1141
|
-
describe("getModelRuntime", () => {
|
|
1142
|
-
it("should throw ModelNotFoundError for invalid modelPath", async () => {
|
|
1143
|
-
sinon.stub(fs, "stat").rejects(new Error("File not found"));
|
|
1144
|
-
|
|
1145
|
-
await expect(async () => {
|
|
1146
|
-
await Model.getModelRuntime(
|
|
1147
|
-
packageName,
|
|
1148
|
-
mockModelPath,
|
|
1149
|
-
new Map(),
|
|
1150
|
-
);
|
|
1151
|
-
}).toThrowError(ModelNotFoundError);
|
|
1152
|
-
|
|
1153
|
-
sinon.restore();
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
});
|
|
1157
|
-
});
|