@malloy-publisher/server 0.0.205 → 0.0.207
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 +394 -395
- package/dist/app/assets/{EnvironmentPage-CAge6UHD.js → EnvironmentPage-BScgHmkw.js} +1 -1
- package/dist/app/assets/{HomePage-DhTe8qpa.js → HomePage-CGedji_w.js} +1 -1
- package/dist/app/assets/{MainPage-CeTxxGex.js → MainPage-DWfF4jXW.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-CpDHB70t.js → MaterializationsPage-B9PDlk8c.js} +1 -1
- package/dist/app/assets/{ModelPage-D9sSMb75.js → ModelPage-BiNOgK_e.js} +1 -1
- package/dist/app/assets/{PackagePage-LRqQWrFY.js → PackagePage-DAN5V7gu.js} +1 -1
- package/dist/app/assets/{RouteError-xT6kuCNw.js → RouteError-CEnIzuKN.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DsIh9svZ.js → WorkbookPage-gA1ceqHP.js} +1 -1
- package/dist/app/assets/{core-C2sQrwVu.es-Bjem0hym.js → core-AOmIKwkc.es-Dclu1Fga.js} +1 -1
- package/dist/app/assets/{index-BdOZDcce.js → index-DGGe8UpP.js} +1 -1
- package/dist/app/assets/{index-RX3QOTde.js → index-DtlPzNxc.js} +127 -127
- package/dist/app/assets/{index-DHHAcY5o.js → index-uu6UpHd2.js} +1 -1
- package/dist/app/assets/{index.umd-D2WH3D-f.js → index.umd-DDq93YX4.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +18 -8
- package/dist/package_load_worker.mjs +19 -2
- package/dist/server.mjs +1272 -1299
- package/package.json +1 -1
- package/src/constants.ts +12 -0
- package/src/controller/materialization.controller.ts +79 -35
- package/src/controller/package.controller.spec.ts +179 -0
- package/src/controller/package.controller.ts +60 -73
- package/src/dto/package.dto.ts +16 -1
- package/src/errors.spec.ts +12 -0
- package/src/errors.ts +18 -0
- package/src/health.spec.ts +34 -1
- package/src/instrumentation.ts +33 -17
- package/src/materialization_metrics.ts +121 -0
- package/src/package_load/package_load_pool.ts +7 -1
- package/src/package_load/package_load_worker.ts +44 -4
- package/src/package_load/protocol.ts +7 -1
- package/src/server-old.ts +7 -149
- package/src/server.ts +9 -188
- package/src/service/authorize_integration.spec.ts +67 -0
- package/src/service/environment.ts +270 -12
- package/src/service/environment_store.spec.ts +0 -81
- package/src/service/environment_store.ts +0 -23
- package/src/service/explore_visibility.spec.ts +434 -0
- package/src/service/exports_probe.spec.ts +107 -0
- package/src/service/manifest_loader.spec.ts +99 -0
- package/src/service/manifest_loader.ts +95 -0
- package/src/service/materialization_service.spec.ts +584 -500
- package/src/service/materialization_service.ts +839 -657
- package/src/service/model.ts +419 -15
- package/src/service/package.spec.ts +14 -2
- package/src/service/package.ts +339 -29
- package/src/service/package_rollback.spec.ts +190 -0
- package/src/service/package_worker_path.spec.ts +223 -0
- package/src/service/query_boundary.spec.ts +470 -0
- package/src/storage/DatabaseInterface.ts +35 -57
- package/src/storage/StorageManager.mock.ts +0 -9
- package/src/storage/StorageManager.ts +7 -290
- package/src/storage/duckdb/DuckDBRepository.ts +2 -35
- package/src/storage/duckdb/MaterializationRepository.ts +52 -27
- package/src/storage/duckdb/schema.ts +4 -20
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +194 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +369 -264
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +7 -4
- package/src/controller/manifest.controller.ts +0 -38
- package/src/service/manifest_service.spec.ts +0 -206
- package/src/service/manifest_service.ts +0 -117
- package/src/service/materialized_table_gc.spec.ts +0 -384
- package/src/service/materialized_table_gc.ts +0 -231
- package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
- package/src/storage/duckdb/ManifestRepository.ts +0 -120
- package/src/storage/duckdb/manifest_store.spec.ts +0 -133
- package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -155
- package/tests/unit/storage/StorageManager.test.ts +0 -166
package/src/server.ts
CHANGED
|
@@ -8,7 +8,6 @@ import * as http from "http";
|
|
|
8
8
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
9
9
|
import { AddressInfo } from "net";
|
|
10
10
|
import * as path from "path";
|
|
11
|
-
import { ParsedQs } from "qs";
|
|
12
11
|
import { fileURLToPath } from "url";
|
|
13
12
|
import { CompileController } from "./controller/compile.controller";
|
|
14
13
|
import { ConnectionController } from "./controller/connection.controller";
|
|
@@ -38,12 +37,10 @@ import { getMemoryGovernorConfig } from "./config";
|
|
|
38
37
|
import { setFilterDeprecationHeaders } from "./filter_deprecation";
|
|
39
38
|
import { checkHeapConfiguration } from "./heap_check";
|
|
40
39
|
import { queryConcurrency } from "./query_concurrency";
|
|
41
|
-
import { ManifestController } from "./controller/manifest.controller";
|
|
42
40
|
import { MaterializationController } from "./controller/materialization.controller";
|
|
43
41
|
import { initializeMcpServer } from "./mcp/server";
|
|
44
42
|
import { registerLegacyRoutes } from "./server-old";
|
|
45
43
|
import { EnvironmentStore } from "./service/environment_store";
|
|
46
|
-
import { ManifestService } from "./service/manifest_service";
|
|
47
44
|
import { MaterializationService } from "./service/materialization_service";
|
|
48
45
|
import { normalizeQueryArray } from "./query_param_utils";
|
|
49
46
|
import { PackageMemoryGovernor } from "./service/package_memory_governor";
|
|
@@ -188,7 +185,6 @@ app.use(httpMetricsMiddleware);
|
|
|
188
185
|
// chase OOMKills before checking the obvious config.
|
|
189
186
|
checkHeapConfiguration();
|
|
190
187
|
const environmentStore = new EnvironmentStore(SERVER_ROOT);
|
|
191
|
-
const manifestService = new ManifestService(environmentStore);
|
|
192
188
|
const watchModeController = new WatchModeController(environmentStore);
|
|
193
189
|
const connectionController = new ConnectionController(environmentStore);
|
|
194
190
|
const modelController = new ModelController(environmentStore);
|
|
@@ -203,24 +199,14 @@ const memoryGovernor = memoryGovernorConfig
|
|
|
203
199
|
: null;
|
|
204
200
|
memoryGovernor?.start();
|
|
205
201
|
environmentStore.setMemoryGovernor(memoryGovernor);
|
|
206
|
-
const packageController = new PackageController(
|
|
207
|
-
environmentStore,
|
|
208
|
-
manifestService,
|
|
209
|
-
);
|
|
202
|
+
const packageController = new PackageController(environmentStore);
|
|
210
203
|
const databaseController = new DatabaseController(environmentStore);
|
|
211
204
|
const queryController = new QueryController(environmentStore);
|
|
212
205
|
const compileController = new CompileController(environmentStore);
|
|
213
|
-
const materializationService = new MaterializationService(
|
|
214
|
-
environmentStore,
|
|
215
|
-
manifestService,
|
|
216
|
-
);
|
|
206
|
+
const materializationService = new MaterializationService(environmentStore);
|
|
217
207
|
const materializationController = new MaterializationController(
|
|
218
208
|
materializationService,
|
|
219
209
|
);
|
|
220
|
-
const manifestController = new ManifestController(
|
|
221
|
-
environmentStore,
|
|
222
|
-
manifestService,
|
|
223
|
-
);
|
|
224
210
|
|
|
225
211
|
export const mcpApp = express();
|
|
226
212
|
|
|
@@ -1056,28 +1042,6 @@ app.get(
|
|
|
1056
1042
|
},
|
|
1057
1043
|
);
|
|
1058
1044
|
|
|
1059
|
-
/**
|
|
1060
|
-
* @deprecated Use /environments/:environmentName/connections/:connectionName/sqlSource POST method instead
|
|
1061
|
-
*/
|
|
1062
|
-
app.get(
|
|
1063
|
-
`${API_PREFIX}/environments/:environmentName/connections/:connectionName/sqlSource`,
|
|
1064
|
-
async (req, res) => {
|
|
1065
|
-
try {
|
|
1066
|
-
res.status(200).json(
|
|
1067
|
-
await connectionController.getConnectionSqlSource(
|
|
1068
|
-
req.params.environmentName,
|
|
1069
|
-
req.params.connectionName,
|
|
1070
|
-
req.query.sqlStatement as string,
|
|
1071
|
-
),
|
|
1072
|
-
);
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
logger.error(error);
|
|
1075
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1076
|
-
res.status(status).json(json);
|
|
1077
|
-
}
|
|
1078
|
-
},
|
|
1079
|
-
);
|
|
1080
|
-
|
|
1081
1045
|
app.post(
|
|
1082
1046
|
`${API_PREFIX}/environments/:environmentName/connections/:connectionName/sqlSource`,
|
|
1083
1047
|
async (req, res) => {
|
|
@@ -1098,26 +1062,6 @@ app.post(
|
|
|
1098
1062
|
);
|
|
1099
1063
|
|
|
1100
1064
|
// Per-package versions
|
|
1101
|
-
app.get(
|
|
1102
|
-
`${API_PREFIX}/environments/:environmentName/packages/:packageName/connections/:connectionName/sqlSource`,
|
|
1103
|
-
async (req, res) => {
|
|
1104
|
-
try {
|
|
1105
|
-
res.status(200).json(
|
|
1106
|
-
await connectionController.getConnectionSqlSource(
|
|
1107
|
-
req.params.environmentName,
|
|
1108
|
-
req.params.connectionName,
|
|
1109
|
-
req.query.sqlStatement as string,
|
|
1110
|
-
req.params.packageName,
|
|
1111
|
-
),
|
|
1112
|
-
);
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
logger.error(error);
|
|
1115
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1116
|
-
res.status(status).json(json);
|
|
1117
|
-
}
|
|
1118
|
-
},
|
|
1119
|
-
);
|
|
1120
|
-
|
|
1121
1065
|
app.post(
|
|
1122
1066
|
`${API_PREFIX}/environments/:environmentName/packages/:packageName/connections/:connectionName/sqlSource`,
|
|
1123
1067
|
async (req, res) => {
|
|
@@ -1152,21 +1096,12 @@ app.post(
|
|
|
1152
1096
|
queryConcurrency(),
|
|
1153
1097
|
async (req, res) => {
|
|
1154
1098
|
try {
|
|
1155
|
-
let options: string | ParsedQs | (string | ParsedQs)[] | undefined;
|
|
1156
|
-
|
|
1157
|
-
// Support both body and query parameters for options for backwards compatibility
|
|
1158
|
-
// TODO: To be removed in the future
|
|
1159
|
-
if (req.body?.options) {
|
|
1160
|
-
options = req.body.options;
|
|
1161
|
-
} else {
|
|
1162
|
-
options = req.query.options;
|
|
1163
|
-
}
|
|
1164
1099
|
res.status(200).json(
|
|
1165
1100
|
await connectionController.getConnectionQueryData(
|
|
1166
1101
|
req.params.environmentName,
|
|
1167
1102
|
req.params.connectionName,
|
|
1168
1103
|
req.body.sqlStatement as string,
|
|
1169
|
-
options as string,
|
|
1104
|
+
req.body?.options as string,
|
|
1170
1105
|
),
|
|
1171
1106
|
);
|
|
1172
1107
|
} catch (error) {
|
|
@@ -1182,65 +1117,12 @@ app.post(
|
|
|
1182
1117
|
queryConcurrency(),
|
|
1183
1118
|
async (req, res) => {
|
|
1184
1119
|
try {
|
|
1185
|
-
let options: string | ParsedQs | (string | ParsedQs)[] | undefined;
|
|
1186
|
-
if (req.body?.options) {
|
|
1187
|
-
options = req.body.options;
|
|
1188
|
-
} else {
|
|
1189
|
-
options = req.query.options;
|
|
1190
|
-
}
|
|
1191
1120
|
res.status(200).json(
|
|
1192
1121
|
await connectionController.getConnectionQueryData(
|
|
1193
1122
|
req.params.environmentName,
|
|
1194
1123
|
req.params.connectionName,
|
|
1195
1124
|
req.body.sqlStatement as string,
|
|
1196
|
-
options as string,
|
|
1197
|
-
req.params.packageName,
|
|
1198
|
-
),
|
|
1199
|
-
);
|
|
1200
|
-
} catch (error) {
|
|
1201
|
-
logger.error(error);
|
|
1202
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1203
|
-
res.status(status).json(json);
|
|
1204
|
-
}
|
|
1205
|
-
},
|
|
1206
|
-
);
|
|
1207
|
-
|
|
1208
|
-
/**
|
|
1209
|
-
* @deprecated Use environments/:environmentName/connections/:connectionName/sqlTemporaryTable POST method instead
|
|
1210
|
-
*/
|
|
1211
|
-
app.get(
|
|
1212
|
-
`${API_PREFIX}/environments/:environmentName/connections/:connectionName/temporaryTable`,
|
|
1213
|
-
queryConcurrency(),
|
|
1214
|
-
async (req, res) => {
|
|
1215
|
-
try {
|
|
1216
|
-
res.status(200).json(
|
|
1217
|
-
await connectionController.getConnectionTemporaryTable(
|
|
1218
|
-
req.params.environmentName,
|
|
1219
|
-
req.params.connectionName,
|
|
1220
|
-
req.query.sqlStatement as string,
|
|
1221
|
-
),
|
|
1222
|
-
);
|
|
1223
|
-
} catch (error) {
|
|
1224
|
-
logger.error(error);
|
|
1225
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1226
|
-
res.status(status).json(json);
|
|
1227
|
-
}
|
|
1228
|
-
},
|
|
1229
|
-
);
|
|
1230
|
-
|
|
1231
|
-
/**
|
|
1232
|
-
* @deprecated Use /environments/:environmentName/packages/:packageName/connections/:connectionName/sqlTemporaryTable
|
|
1233
|
-
*/
|
|
1234
|
-
app.get(
|
|
1235
|
-
`${API_PREFIX}/environments/:environmentName/packages/:packageName/connections/:connectionName/temporaryTable`,
|
|
1236
|
-
queryConcurrency(),
|
|
1237
|
-
async (req, res) => {
|
|
1238
|
-
try {
|
|
1239
|
-
res.status(200).json(
|
|
1240
|
-
await connectionController.getConnectionTemporaryTable(
|
|
1241
|
-
req.params.environmentName,
|
|
1242
|
-
req.params.connectionName,
|
|
1243
|
-
req.query.sqlStatement as string,
|
|
1125
|
+
req.body?.options as string,
|
|
1244
1126
|
req.params.packageName,
|
|
1245
1127
|
),
|
|
1246
1128
|
);
|
|
@@ -1317,11 +1199,9 @@ app.post(
|
|
|
1317
1199
|
`${API_PREFIX}/environments/:environmentName/packages`,
|
|
1318
1200
|
async (req, res) => {
|
|
1319
1201
|
try {
|
|
1320
|
-
const autoLoadManifest = req.query.autoLoadManifest === "true";
|
|
1321
1202
|
const _package = await packageController.addPackage(
|
|
1322
1203
|
req.params.environmentName,
|
|
1323
1204
|
req.body,
|
|
1324
|
-
{ autoLoadManifest },
|
|
1325
1205
|
);
|
|
1326
1206
|
res.status(200).json(_package?.getPackageMetadata());
|
|
1327
1207
|
} catch (error) {
|
|
@@ -1705,33 +1585,17 @@ app.get(
|
|
|
1705
1585
|
},
|
|
1706
1586
|
);
|
|
1707
1587
|
|
|
1708
|
-
app.post(
|
|
1709
|
-
`${API_PREFIX}/environments/:environmentName/packages/:packageName/materializations/teardown`,
|
|
1710
|
-
async (req, res) => {
|
|
1711
|
-
try {
|
|
1712
|
-
const result = await materializationController.teardownPackage(
|
|
1713
|
-
req.params.environmentName,
|
|
1714
|
-
req.params.packageName,
|
|
1715
|
-
req.body || {},
|
|
1716
|
-
);
|
|
1717
|
-
res.status(200).json(result);
|
|
1718
|
-
} catch (error) {
|
|
1719
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1720
|
-
res.status(status).json(json);
|
|
1721
|
-
}
|
|
1722
|
-
},
|
|
1723
|
-
);
|
|
1724
|
-
|
|
1725
1588
|
app.post(
|
|
1726
1589
|
`${API_PREFIX}/environments/:environmentName/packages/:packageName/materializations/:materializationId`,
|
|
1727
1590
|
async (req, res) => {
|
|
1728
1591
|
try {
|
|
1729
1592
|
const action = req.query.action;
|
|
1730
|
-
if (action === "
|
|
1731
|
-
const build = await materializationController.
|
|
1593
|
+
if (action === "build") {
|
|
1594
|
+
const build = await materializationController.buildMaterialization(
|
|
1732
1595
|
req.params.environmentName,
|
|
1733
1596
|
req.params.packageName,
|
|
1734
1597
|
req.params.materializationId,
|
|
1598
|
+
req.body || {},
|
|
1735
1599
|
);
|
|
1736
1600
|
res.status(202).json(build);
|
|
1737
1601
|
} else if (action === "stop") {
|
|
@@ -1743,7 +1607,7 @@ app.post(
|
|
|
1743
1607
|
res.status(200).json(build);
|
|
1744
1608
|
} else {
|
|
1745
1609
|
throw new BadRequestError(
|
|
1746
|
-
`Unsupported action '${String(action ?? "")}'. Expected '
|
|
1610
|
+
`Unsupported action '${String(action ?? "")}'. Expected 'build' or 'stop'.`,
|
|
1747
1611
|
);
|
|
1748
1612
|
}
|
|
1749
1613
|
} catch (error) {
|
|
@@ -1761,6 +1625,7 @@ app.delete(
|
|
|
1761
1625
|
req.params.environmentName,
|
|
1762
1626
|
req.params.packageName,
|
|
1763
1627
|
req.params.materializationId,
|
|
1628
|
+
{ dropTables: req.query.dropTables === "true" },
|
|
1764
1629
|
);
|
|
1765
1630
|
res.status(204).send();
|
|
1766
1631
|
} catch (error) {
|
|
@@ -1770,49 +1635,6 @@ app.delete(
|
|
|
1770
1635
|
},
|
|
1771
1636
|
);
|
|
1772
1637
|
|
|
1773
|
-
// ==================== MANIFEST ROUTES ====================
|
|
1774
|
-
|
|
1775
|
-
app.get(
|
|
1776
|
-
`${API_PREFIX}/environments/:environmentName/packages/:packageName/manifest`,
|
|
1777
|
-
async (req, res) => {
|
|
1778
|
-
try {
|
|
1779
|
-
const manifest = await manifestController.getManifest(
|
|
1780
|
-
req.params.environmentName,
|
|
1781
|
-
req.params.packageName,
|
|
1782
|
-
);
|
|
1783
|
-
res.status(200).json(manifest);
|
|
1784
|
-
} catch (error) {
|
|
1785
|
-
logger.error("Get manifest error", { error });
|
|
1786
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1787
|
-
res.status(status).json(json);
|
|
1788
|
-
}
|
|
1789
|
-
},
|
|
1790
|
-
);
|
|
1791
|
-
|
|
1792
|
-
app.post(
|
|
1793
|
-
`${API_PREFIX}/environments/:environmentName/packages/:packageName/manifest`,
|
|
1794
|
-
async (req, res) => {
|
|
1795
|
-
try {
|
|
1796
|
-
const action = req.query.action;
|
|
1797
|
-
if (action === "reload") {
|
|
1798
|
-
const manifest = await manifestController.reloadManifest(
|
|
1799
|
-
req.params.environmentName,
|
|
1800
|
-
req.params.packageName,
|
|
1801
|
-
);
|
|
1802
|
-
res.status(200).json(manifest);
|
|
1803
|
-
} else {
|
|
1804
|
-
throw new BadRequestError(
|
|
1805
|
-
`Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
} catch (error) {
|
|
1809
|
-
logger.error("Manifest action error", { error });
|
|
1810
|
-
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1811
|
-
res.status(status).json(json);
|
|
1812
|
-
}
|
|
1813
|
-
},
|
|
1814
|
-
);
|
|
1815
|
-
|
|
1816
1638
|
// Register legacy `/projects/...` routes for backwards compatibility with
|
|
1817
1639
|
// clients that haven't migrated to `/environments/...` yet. Must be added
|
|
1818
1640
|
// before the SPA catch-all below.
|
|
@@ -1825,7 +1647,6 @@ registerLegacyRoutes(app, {
|
|
|
1825
1647
|
queryController,
|
|
1826
1648
|
compileController,
|
|
1827
1649
|
materializationController,
|
|
1828
|
-
manifestController,
|
|
1829
1650
|
});
|
|
1830
1651
|
|
|
1831
1652
|
// Modify the catch-all route to only serve index.html in production
|
|
@@ -930,3 +930,70 @@ source: declared is duckdb.table('customers') extend { measure: c is count() }
|
|
|
930
930
|
).resolves.toBeUndefined();
|
|
931
931
|
});
|
|
932
932
|
});
|
|
933
|
+
|
|
934
|
+
// A given can be both a runtime query parameter (substituted into a view's
|
|
935
|
+
// `where`) AND the subject of an `#(authorize)` gate on the same source. These
|
|
936
|
+
// two layers compose: the gate fires first (before any filtering), and once it
|
|
937
|
+
// passes the parameterized filter applies to the result. This is the layered
|
|
938
|
+
// case the end-to-end story rests on.
|
|
939
|
+
describe("authorize composes with given parameters", () => {
|
|
940
|
+
// `ROLE` gates access; `REGION` is a runtime filter parameter on the view.
|
|
941
|
+
const COMPOSED = `##! experimental.givens
|
|
942
|
+
|
|
943
|
+
given:
|
|
944
|
+
ROLE :: string
|
|
945
|
+
REGION :: string
|
|
946
|
+
|
|
947
|
+
#(authorize) "$ROLE = 'analyst'"
|
|
948
|
+
source: regional is duckdb.table('customers') extend {
|
|
949
|
+
measure: c is count()
|
|
950
|
+
view: in_region is {
|
|
951
|
+
where: region = $REGION
|
|
952
|
+
aggregate: c
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
`;
|
|
956
|
+
|
|
957
|
+
async function runComposed(givens: Record<string, GivenValue>) {
|
|
958
|
+
await writeModel("compose.malloy", COMPOSED);
|
|
959
|
+
const model = await Model.create(
|
|
960
|
+
"test-pkg",
|
|
961
|
+
TEST_PKG_DIR,
|
|
962
|
+
"compose.malloy",
|
|
963
|
+
getConnections(),
|
|
964
|
+
);
|
|
965
|
+
return model.getQueryResults(
|
|
966
|
+
undefined,
|
|
967
|
+
undefined,
|
|
968
|
+
"run: regional -> in_region",
|
|
969
|
+
undefined,
|
|
970
|
+
undefined,
|
|
971
|
+
givens,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function countOf(compactResult: unknown): number {
|
|
976
|
+
const rows = compactResult as Array<Record<string, unknown>>;
|
|
977
|
+
return Number(rows[0]?.c);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
it("denies before filtering when the gate fails, even with the filter given supplied", async () => {
|
|
981
|
+
await expect(runComposed({ REGION: "us-west" })).rejects.toBeInstanceOf(
|
|
982
|
+
AccessDeniedError,
|
|
983
|
+
);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("applies the parameterized filter once the gate passes", async () => {
|
|
987
|
+
// Seed: one 'us-west' customer, one 'us-east'.
|
|
988
|
+
const west = await runComposed({ ROLE: "analyst", REGION: "us-west" });
|
|
989
|
+
expect(countOf(west.compactResult)).toBe(1);
|
|
990
|
+
|
|
991
|
+
const east = await runComposed({ ROLE: "analyst", REGION: "us-east" });
|
|
992
|
+
expect(countOf(east.compactResult)).toBe(1);
|
|
993
|
+
|
|
994
|
+
// A region matching no rows proves the given genuinely filters (not a
|
|
995
|
+
// gate-only pass-through).
|
|
996
|
+
const none = await runComposed({ ROLE: "analyst", REGION: "nowhere" });
|
|
997
|
+
expect(countOf(none.compactResult)).toBe(0);
|
|
998
|
+
});
|
|
999
|
+
});
|