@malloy-publisher/server 0.0.228 → 0.0.230
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 +102 -10
- package/dist/app/assets/EnvironmentPage-wa_EPkwK.js +1 -0
- package/dist/app/assets/{HomePage-Bkwc9Woc.js → HomePage-jnCrupQp.js} +1 -1
- package/dist/app/assets/{LightMode-Bum_KBpN.js → LightMode-DYbwNULZ.js} +1 -1
- package/dist/app/assets/MainPage-CuJLrPNI.js +2 -0
- package/dist/app/assets/MaterializationsPage-D_67x2ee.js +1 -0
- package/dist/app/assets/ModelPage-D5JtAWqR.js +1 -0
- package/dist/app/assets/PackagePage-BRwtqUSG.js +1 -0
- package/dist/app/assets/{RouteError-DmJLpLXm.js → RouteError-CBNNrnSD.js} +1 -1
- package/dist/app/assets/ThemeEditorPage-CTCeBneA.js +1 -0
- package/dist/app/assets/WorkbookPage-SN6f1RBm.js +1 -0
- package/dist/app/assets/{core-CEDZMHV1.es-_yGzNgNH.js → core-Dp3q5Ieu.es-CD5FvM2s.js} +10 -10
- package/dist/app/assets/{index-CdmFub34.js → index-B3Nn8Vm2.js} +459 -446
- package/dist/app/assets/index-BLCx1EdC.js +18 -0
- package/dist/app/assets/index-C_tJstcx.js +529 -0
- package/dist/app/assets/{index-CE9xhdra.js → index-CfmBVB6M.js} +1 -1
- package/dist/app/assets/{index-OEjKNSYb.js → index-DU4r7GdU.js} +573 -413
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +1 -1
- package/dist/package_load_worker.mjs +109 -8
- package/dist/server.mjs +7504 -80
- package/package.json +15 -13
- package/scripts/bake-duckdb-extensions.js +5 -2
- package/src/config.spec.ts +87 -1
- package/src/config.ts +142 -1
- package/src/controller/materialization.controller.spec.ts +9 -0
- package/src/controller/materialization.controller.ts +15 -0
- package/src/ducklake_version.spec.ts +163 -0
- package/src/ducklake_version.ts +153 -0
- package/src/errors.ts +12 -0
- package/src/malloy_pin_prereqs.spec.ts +25 -0
- package/src/materialization_metrics.ts +33 -0
- package/src/mcp/skills/build_skills_bundle.ts +29 -9
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/skills/skills_bundle.spec.ts +31 -2
- package/src/query_param_utils.ts +11 -0
- package/src/server.ts +74 -9
- package/src/service/authorize.spec.ts +22 -0
- package/src/service/authorize.ts +267 -10
- package/src/service/authorize_integration.spec.ts +1068 -26
- package/src/service/connection.spec.ts +79 -7
- package/src/service/connection.ts +312 -25
- package/src/service/connection_config.spec.ts +21 -0
- package/src/service/connection_config.ts +15 -1
- package/src/service/cron_evaluator.spec.ts +79 -0
- package/src/service/cron_evaluator.ts +105 -0
- package/src/service/ducklake_lazy_attach.spec.ts +110 -0
- package/src/service/environment.ts +156 -17
- package/src/service/environment_store.spec.ts +569 -14
- package/src/service/environment_store.ts +237 -31
- package/src/service/environment_store_anchoring.spec.ts +107 -0
- package/src/service/extension_fetch_policy.spec.ts +256 -0
- package/src/service/manifest_loader.spec.ts +3 -1
- package/src/service/manifest_loader.ts +9 -5
- package/src/service/materialization_scheduler.spec.ts +289 -0
- package/src/service/materialization_scheduler.ts +304 -0
- package/src/service/materialization_service.spec.ts +286 -5
- package/src/service/materialization_service.ts +121 -8
- package/src/service/materialization_test_fixtures.ts +10 -1
- package/src/service/model.spec.ts +67 -0
- package/src/service/model.ts +656 -31
- package/src/service/package.spec.ts +38 -0
- package/src/service/package.ts +102 -2
- package/src/service/package_quote_bound_tables.spec.ts +171 -0
- package/src/service/persistence_policy.spec.ts +31 -0
- package/src/service/quoting.spec.ts +57 -1
- package/src/service/quoting.ts +33 -0
- package/src/storage/DatabaseInterface.ts +15 -0
- package/src/storage/duckdb/DuckDBRepository.ts +17 -0
- package/src/storage/duckdb/MaterializationRepository.spec.ts +191 -0
- package/src/storage/duckdb/MaterializationRepository.ts +68 -0
- package/tests/fixtures/persist-schedule-test/data/orders.csv +5 -0
- package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +11 -0
- package/tests/fixtures/persist-schedule-test/publisher.json +7 -0
- package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +5 -0
- package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +11 -0
- package/tests/fixtures/persist-schedule-warn-test/publisher.json +6 -0
- package/tests/integration/materialization/environment_list.integration.spec.ts +133 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +7 -2
- package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +150 -0
- package/tests/integration/materialization/schedule_edit.integration.spec.ts +126 -0
- package/tests/integration/materialization/scheduler.integration.spec.ts +200 -0
- package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +172 -0
- package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +256 -0
- package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +143 -0
- package/dist/app/assets/EnvironmentPage-EW2lbGvb.js +0 -1
- package/dist/app/assets/MainPage-oiEy7TNM.js +0 -2
- package/dist/app/assets/MaterializationsPage-C_VJsTgU.js +0 -1
- package/dist/app/assets/ModelPage-z8REqAmk.js +0 -1
- package/dist/app/assets/PackagePage-C2Vtt1Ln.js +0 -1
- package/dist/app/assets/ThemeEditorPage-BywFjC7A.js +0 -1
- package/dist/app/assets/WorkbookPage-DCMizDMR.js +0 -1
- package/dist/app/assets/index-DDMrjIT3.js +0 -18
- package/dist/app/assets/index-EqslXZ44.js +0 -527
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
MaterializationMode,
|
|
15
15
|
recordAutoLoadOutcome,
|
|
16
16
|
recordDropTables,
|
|
17
|
+
recordManifestBindDegraded,
|
|
17
18
|
recordMaterializationRun,
|
|
18
19
|
recordSourceBuildDuration,
|
|
19
20
|
recordSourcesOutcome,
|
|
20
21
|
} from "../materialization_metrics";
|
|
21
22
|
import {
|
|
22
23
|
BuildInstruction,
|
|
23
|
-
BuildManifest,
|
|
24
24
|
BuildManifestResult,
|
|
25
25
|
BuildPlan,
|
|
26
26
|
FreshnessManifest,
|
|
@@ -41,7 +41,12 @@ import {
|
|
|
41
41
|
iterGraphSources,
|
|
42
42
|
} from "./build_plan";
|
|
43
43
|
import { EnvironmentStore } from "./environment_store";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
bareTableName,
|
|
46
|
+
quoteIdentifier,
|
|
47
|
+
quoteManifestTablePath,
|
|
48
|
+
quoteTablePath,
|
|
49
|
+
} from "./quoting";
|
|
45
50
|
import { resolveEnvironmentId } from "./resolve_environment";
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -177,6 +182,38 @@ export class MaterializationService {
|
|
|
177
182
|
);
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Every materialization across all packages in an environment, newest first.
|
|
187
|
+
* Each record carries its `packageName`, so an env-scoped view can group or
|
|
188
|
+
* label by package without a per-package fan-out.
|
|
189
|
+
*/
|
|
190
|
+
async listEnvironmentMaterializations(
|
|
191
|
+
environmentName: string,
|
|
192
|
+
options?: { limit?: number; offset?: number },
|
|
193
|
+
): Promise<Materialization[]> {
|
|
194
|
+
const environmentId = await this.resolveEnvironmentId(environmentName);
|
|
195
|
+
return this.repository.listMaterializationsByEnvironment(
|
|
196
|
+
environmentId,
|
|
197
|
+
options,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `created_at` of the newest scheduler-fired materialization for a package,
|
|
203
|
+
* or null if none. The standalone scheduler uses this on its first arm to
|
|
204
|
+
* recover a fire missed during downtime (see MaterializationScheduler.arm).
|
|
205
|
+
*/
|
|
206
|
+
async getLatestScheduledFireAt(
|
|
207
|
+
environmentName: string,
|
|
208
|
+
packageName: string,
|
|
209
|
+
): Promise<Date | null> {
|
|
210
|
+
const environmentId = await this.resolveEnvironmentId(environmentName);
|
|
211
|
+
return this.repository.getLatestScheduledFireAt(
|
|
212
|
+
environmentId,
|
|
213
|
+
packageName,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
180
217
|
async getMaterialization(
|
|
181
218
|
environmentName: string,
|
|
182
219
|
packageName: string,
|
|
@@ -219,6 +256,13 @@ export class MaterializationService {
|
|
|
219
256
|
buildInstructions?: BuildInstruction[];
|
|
220
257
|
referenceManifest?: ManifestReference[];
|
|
221
258
|
strictUpstreams?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* What initiated this run. `ON_DEMAND` (default) = a manual/API create;
|
|
261
|
+
* `SCHEDULER` = the standalone materialization scheduler firing a
|
|
262
|
+
* package's `materialization.schedule` cron. Recorded on the run
|
|
263
|
+
* metadata so a scheduled rebuild is distinguishable from a manual one.
|
|
264
|
+
*/
|
|
265
|
+
trigger?: "ON_DEMAND" | "SCHEDULER";
|
|
222
266
|
} = {},
|
|
223
267
|
): Promise<Materialization> {
|
|
224
268
|
const environmentId = await this.resolveEnvironmentId(environmentName);
|
|
@@ -244,10 +288,12 @@ export class MaterializationService {
|
|
|
244
288
|
}
|
|
245
289
|
|
|
246
290
|
const forceRefresh = options.forceRefresh ?? false;
|
|
291
|
+
const trigger = options.trigger ?? "ON_DEMAND";
|
|
247
292
|
const metadata = {
|
|
248
293
|
forceRefresh,
|
|
249
294
|
sourceNames: options.sourceNames ?? null,
|
|
250
295
|
mode: orchestrated ? "orchestrated" : "auto",
|
|
296
|
+
trigger,
|
|
251
297
|
};
|
|
252
298
|
|
|
253
299
|
let created: Materialization;
|
|
@@ -280,6 +326,7 @@ export class MaterializationService {
|
|
|
280
326
|
buildInstructions,
|
|
281
327
|
referenceManifest: options.referenceManifest,
|
|
282
328
|
strictUpstreams: options.strictUpstreams,
|
|
329
|
+
trigger,
|
|
283
330
|
},
|
|
284
331
|
signal,
|
|
285
332
|
),
|
|
@@ -306,6 +353,7 @@ export class MaterializationService {
|
|
|
306
353
|
buildInstructions: BuildInstruction[] | undefined;
|
|
307
354
|
referenceManifest: ManifestReference[] | undefined;
|
|
308
355
|
strictUpstreams: boolean | undefined;
|
|
356
|
+
trigger: "ON_DEMAND" | "SCHEDULER";
|
|
309
357
|
},
|
|
310
358
|
signal: AbortSignal,
|
|
311
359
|
): Promise<void> {
|
|
@@ -379,6 +427,7 @@ export class MaterializationService {
|
|
|
379
427
|
forceRefresh: opts.forceRefresh,
|
|
380
428
|
sourceNames: opts.sourceNames ?? null,
|
|
381
429
|
mode,
|
|
430
|
+
trigger: opts.trigger,
|
|
382
431
|
sourcesBuilt,
|
|
383
432
|
sourcesReused,
|
|
384
433
|
durationMs,
|
|
@@ -466,9 +515,13 @@ export class MaterializationService {
|
|
|
466
515
|
/**
|
|
467
516
|
* Project the caller-supplied upstream reference manifest into the seed
|
|
468
517
|
* entry map `executeInstructedBuild` consumes. Each reference is keyed by the
|
|
469
|
-
* compiler's manifest-lookup sourceEntityId and carries
|
|
470
|
-
*
|
|
471
|
-
* reference to the existing table
|
|
518
|
+
* compiler's manifest-lookup sourceEntityId and carries the physical table
|
|
519
|
+
* name plus the connection it lives on — enough for the build Manifest to
|
|
520
|
+
* resolve a downstream persist reference to the existing table, and to quote
|
|
521
|
+
* that reference for the connection's dialect so it resolves on a
|
|
522
|
+
* case-folding engine (see {@link quoteSeedTablePath}). `connectionName` is
|
|
523
|
+
* optional on the wire: an older control plane that omits it seeds unquoted,
|
|
524
|
+
* exactly as before.
|
|
472
525
|
*/
|
|
473
526
|
private referenceManifestToEntries(
|
|
474
527
|
referenceManifest: ManifestReference[] | undefined,
|
|
@@ -478,6 +531,7 @@ export class MaterializationService {
|
|
|
478
531
|
entries[ref.sourceEntityId] = {
|
|
479
532
|
sourceEntityId: ref.sourceEntityId,
|
|
480
533
|
physicalTableName: ref.physicalTableName,
|
|
534
|
+
connectionName: ref.connectionName,
|
|
481
535
|
};
|
|
482
536
|
}
|
|
483
537
|
return entries;
|
|
@@ -524,11 +578,17 @@ export class MaterializationService {
|
|
|
524
578
|
// stamps freshness (dataAsOf/window/fallback) on the wire manifest it
|
|
525
579
|
// distributes, not on this in-memory post-build load, so these sources are
|
|
526
580
|
// bound un-gated (always serve the freshly-built table).
|
|
527
|
-
const manifestEntries:
|
|
581
|
+
const manifestEntries: FreshnessManifest = {};
|
|
528
582
|
for (const [sourceEntityId, entry] of Object.entries(entries)) {
|
|
529
583
|
if (entry.physicalTableName) {
|
|
530
584
|
manifestEntries[sourceEntityId] = {
|
|
531
585
|
tableName: entry.physicalTableName,
|
|
586
|
+
// Carried so the bind step can quote the physical path for the
|
|
587
|
+
// connection's dialect (Package.quoteBoundTableNames) — the
|
|
588
|
+
// build CREATEd it quoted, so an unquoted read would miss on a
|
|
589
|
+
// case-folding engine in the window before the control plane's
|
|
590
|
+
// wire-manifest rebind.
|
|
591
|
+
connectionName: entry.connectionName,
|
|
532
592
|
};
|
|
533
593
|
}
|
|
534
594
|
}
|
|
@@ -586,6 +646,43 @@ export class MaterializationService {
|
|
|
586
646
|
}
|
|
587
647
|
}
|
|
588
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Quote a seeded upstream's physical path for the in-memory build Manifest,
|
|
651
|
+
* mirroring the CREATE side ({@link quoteManifestTablePath}) so a downstream
|
|
652
|
+
* persist source resolves the reference on a case-folding engine. The seed's
|
|
653
|
+
* own connection (carried on the entry) supplies the dialect. If it can't be
|
|
654
|
+
* resolved — an older control plane omitted `connectionName`, or the named
|
|
655
|
+
* connection isn't part of this build — the path binds unquoted: the build
|
|
656
|
+
* then fails loudly at the downstream CREATE on a case-folding engine (a
|
|
657
|
+
* self-signaling miss, unlike the serve path), and is unaffected elsewhere.
|
|
658
|
+
*/
|
|
659
|
+
private quoteSeedTablePath(
|
|
660
|
+
sourceEntityId: string,
|
|
661
|
+
physicalTableName: string,
|
|
662
|
+
connectionName: string | undefined,
|
|
663
|
+
connections: Map<string, MalloyConnection>,
|
|
664
|
+
): string {
|
|
665
|
+
const connection = connectionName
|
|
666
|
+
? connections.get(connectionName)
|
|
667
|
+
: undefined;
|
|
668
|
+
if (!connection) {
|
|
669
|
+
// A named-but-absent connection is a real gap (the seed can't be
|
|
670
|
+
// quoted); a missing name is the benign older-CP default. Only the
|
|
671
|
+
// former is worth a signal.
|
|
672
|
+
if (connectionName) {
|
|
673
|
+
recordManifestBindDegraded();
|
|
674
|
+
logger.warn(
|
|
675
|
+
"Seeded upstream names a connection not present in this build; " +
|
|
676
|
+
"leaving its manifest path unquoted (a downstream build will " +
|
|
677
|
+
"fail on a case-folding engine)",
|
|
678
|
+
{ sourceEntityId, connectionName },
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
return physicalTableName;
|
|
682
|
+
}
|
|
683
|
+
return quoteManifestTablePath(physicalTableName, connection.dialectName);
|
|
684
|
+
}
|
|
685
|
+
|
|
589
686
|
/**
|
|
590
687
|
* Shared build loop for both auto-run and orchestrated builds. Seeds the
|
|
591
688
|
* manifest with carried-forward (reused) upstream entries so downstream
|
|
@@ -630,8 +727,19 @@ export class MaterializationService {
|
|
|
630
727
|
const entries: Record<string, ManifestEntry> = {};
|
|
631
728
|
for (const [sourceEntityId, entry] of Object.entries(seedEntries)) {
|
|
632
729
|
if (entry.physicalTableName) {
|
|
730
|
+
// The build Manifest feeds a downstream persist's `FROM` verbatim,
|
|
731
|
+
// so a seeded upstream must carry the SAME quoting the builder
|
|
732
|
+
// CREATEd it with — else the downstream CREATE misses the
|
|
733
|
+
// case-preserved table on a case-folding engine. The seed keeps its
|
|
734
|
+
// logical (unquoted) name in `entries` (the committed manifest, in
|
|
735
|
+
// logical-name space); only the in-memory build Manifest is quoted.
|
|
633
736
|
manifest.update(sourceEntityId, {
|
|
634
|
-
tableName:
|
|
737
|
+
tableName: this.quoteSeedTablePath(
|
|
738
|
+
sourceEntityId,
|
|
739
|
+
entry.physicalTableName,
|
|
740
|
+
entry.connectionName,
|
|
741
|
+
connections,
|
|
742
|
+
),
|
|
635
743
|
});
|
|
636
744
|
}
|
|
637
745
|
entries[sourceEntityId] = entry;
|
|
@@ -733,7 +841,12 @@ export class MaterializationService {
|
|
|
733
841
|
}
|
|
734
842
|
|
|
735
843
|
// Make this table visible to downstream sources built later in this run.
|
|
736
|
-
|
|
844
|
+
// Record the SAME quoted path the CREATE used (not the logical name): the
|
|
845
|
+
// build Manifest is pasted into a downstream `FROM` verbatim, so on a
|
|
846
|
+
// case-folding engine an unquoted name would miss the case-preserved table
|
|
847
|
+
// just written. The returned entry (below) keeps the logical name for the
|
|
848
|
+
// committed manifest; only this in-memory build Manifest is quoted.
|
|
849
|
+
manifest.update(sourceEntityId, { tableName: quotedPhysical });
|
|
737
850
|
|
|
738
851
|
const durationMs = Math.round(performance.now() - startTime);
|
|
739
852
|
recordSourceBuildDuration(durationMs);
|
|
@@ -120,6 +120,12 @@ export function fakeSource(opts: {
|
|
|
120
120
|
freshnessSchedule?: FakeFreshnessSchedule;
|
|
121
121
|
/** Model-file-level (`##`) freshness default. */
|
|
122
122
|
modelFreshnessSchedule?: FakeFreshnessSchedule;
|
|
123
|
+
/**
|
|
124
|
+
* Spy on the args Malloy's SQL generation is handed — chiefly the
|
|
125
|
+
* `buildManifest` that resolves upstream persist references — so a test can
|
|
126
|
+
* assert what physical name a downstream build sees for its upstream.
|
|
127
|
+
*/
|
|
128
|
+
onGetSQL?: (sqlOpts: unknown) => void;
|
|
123
129
|
}): PersistSource {
|
|
124
130
|
const fields = opts.annotationFields;
|
|
125
131
|
return {
|
|
@@ -128,7 +134,10 @@ export function fakeSource(opts: {
|
|
|
128
134
|
connectionName: opts.connectionName ?? "duckdb",
|
|
129
135
|
dialectName: opts.dialectName ?? "duckdb",
|
|
130
136
|
makeBuildId: () => opts.sourceEntityId,
|
|
131
|
-
getSQL: () =>
|
|
137
|
+
getSQL: (sqlOpts?: unknown) => {
|
|
138
|
+
opts.onGetSQL?.(sqlOpts);
|
|
139
|
+
return opts.sql ?? "SELECT 1";
|
|
140
|
+
},
|
|
132
141
|
annotations: {
|
|
133
142
|
parseAsTag: () => ({
|
|
134
143
|
tag: fakeTag(fields, opts.freshnessSchedule),
|
|
@@ -4,6 +4,7 @@ import fs from "fs/promises";
|
|
|
4
4
|
import sinon from "sinon";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
AccessDeniedError,
|
|
7
8
|
BadRequestError,
|
|
8
9
|
ModelNotFoundError,
|
|
9
10
|
PayloadTooLargeError,
|
|
@@ -420,6 +421,11 @@ describe("service/model", () => {
|
|
|
420
421
|
undefined,
|
|
421
422
|
undefined,
|
|
422
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,
|
|
423
429
|
);
|
|
424
430
|
|
|
425
431
|
await expect(
|
|
@@ -716,6 +722,11 @@ describe("service/model", () => {
|
|
|
716
722
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
717
723
|
runnableCells as any,
|
|
718
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,
|
|
719
730
|
);
|
|
720
731
|
|
|
721
732
|
await expect(
|
|
@@ -819,6 +830,62 @@ describe("service/model", () => {
|
|
|
819
830
|
});
|
|
820
831
|
});
|
|
821
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
|
+
|
|
822
889
|
describe("static methods", () => {
|
|
823
890
|
describe("getModelRuntime", () => {
|
|
824
891
|
it("should throw ModelNotFoundError for invalid modelPath", async () => {
|