Skip to content

Commit 6210f88

Browse files
fix(lint): field-no-consumers credits every field an analytics member's column path reads (#21439) (#21460)
Fixes #21439 Clause-②: no ## What changed `field-no-consumers` (`packages/lint/src/validate-field-consumers.ts`) now owns the four analytics slots that name a column: a dataset dimension's and measure's `field`, and a cube dimension's and measure's `sql`. Each path credits every field it reads. Those fields are the lookup on the base object, each intermediate lookup, and the column on the object the last hop reaches. They are the same fields the analytics door's field-level read gate names (`fieldsOfColumnSql` in `service-analytics`). - **One hop resolver.** A cube's path goes through `resolveCubeColumn`, the resolver the cube leg of `validate-dataset-measure-aggregates.ts` already uses: the cube's declared join for the hop, else the lookup's `reference`. That function is now `export`ed from its module. It is not re-exported from the package barrel: `resolveCubeColumn` appears 0 times in `dist/index.d.ts`, `dist/index.d.cts` and `dist/runtime.d.ts`. A dataset's path goes through `resolveFieldPath` plus `joinablePrefixes(include)`, the pair `validate-dataset-references.ts` uses. The rule asks the resolver about each prefix of the path (`account`, then `account.region`, then `account.region.zone`). The leaf of each prefix is one field the path reads, so the rule walks no hop itself. The cube leg's body is unchanged: its diff is one `export` keyword and one docblock paragraph, so nothing it refuses changes. - **One reading per slot.** The general text walk now skips those four config paths. It read a dotted path as one token and credited neither end. It also credited nothing for a bare cube column, because a cube names its object in its own `sql` and the walk's object context never reads that. - **Paths the door does not read.** A path counts as read only when every prefix resolves and, on a dataset, `include` declares the relationship prefix. Otherwise: - The door refuses a path whose hop or column does not resolve, and a dataset path whose join `include` does not declare. (`compileDataset` refuses the dataset, and `dataset-field-not-included` already reports it.) Each field such a path names is recorded as a **carrier**, not a reader. That is the rule's existing word for a site that names a field and reads it nowhere: an `inlineColumns` entry with no `inlineEdit` is the precedent. So the verdict is `carrier-only` and the finding lists the member path. It is not a false "inert — no site of any kind names it", and it is not a guessed read. - The object graph cannot judge some paths: a lookup to an object this stack does not define, a hop through an injected column, a relationship with no target. Such a path credits the fields it does resolve. The door joins through them, and "cannot answer" is no evidence that nothing reads them. - The row wildcard `'*'` reads no field and credits none. - **Ruling B (`5950947151`) stands.** Only explicit, authored member paths are credited. Nothing drawn by default is counted. - The finding's message names cube members among the consumers, and refused analytics paths among the carriers. - One `@objectstack/lint` **patch** changeset. That is the family's convention for the lint side of a false-"inert" fix: the four earlier changesets of this family were lint patch three times and minor once, the minor being the first, which added two consumer kinds. A false advisory verdict in a released package is a bug fix. No export reaches the published entry points. ## Evidence (final commit `6ae7c521fe`) **`os validate`, before and after**, on a scratch fixture that is not committed. Three objects: `fx_ledger.account` is a lookup to `fx_account`, and `fx_account.region` is a lookup to `fx_region`. Fields prefixed `c_` are read only by a cube member, `d_` only by a dataset member (`include: ['d_account.d_region']`), and `u_` only by a dataset measure whose join is not declared. Each slot has a bare, a one-hop and a two-hop member. | | before (CLI and lint built at BASE `53fd35e3e3`) | after (lint rebuilt at `51819a6aaf`; later commits are comment-only) | |:--|:--|:--| | cube: `c_kind`, `c_amount` (bare) | inert | not reported | | cube: `c_account`, `c_tier`, `c_revenue`, `c_region`, `c_code`, `c_zone` | inert | not reported | | dataset: `d_kind`, `d_amount` (bare) | not reported | not reported | | dataset: `d_account`, `d_tier`, `d_revenue`, `d_region`, `d_code`, `d_zone` | inert | not reported | | undeclared join: `u_account`, `u_revenue` | inert | `carrier-only`, carrier `datasets[1].measures[0].field` | Both runs exit 1 on the fixture's one error, `dataset-field-not-included` on the undeclared measure. The `field-no-consumers` warnings went from 16 to 2. **Tests.** - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`: 119 files and 5619 tests passed. - `pnpm --filter @objectstack/lint typecheck`: exit 0, `check:test-typecheck: OK`. The test file is in the `tsconfig.test.json` program (`--listFiles` count 1), with 0 errors in it. - The rule's test file has 153 tests, 27 of them new. Among them is the **enumeration pin**: 4 slots × 3 shapes, each asserting that exactly the fields the path reads leave the report. The slot list is read off the spec's own `CubeSchema` and `DatasetSchema` through `z.toJSONSchema`: every string whose pattern is the analytics column path. It is held equal to the pin's builders, so a column slot the spec adds on either root fails the pin. **Ablations.** Each leg ran on the committed tree through `scripts/ablation-replace.mjs` with a `trap` restore. Every anchor hit 1 time and then 0. Every restore was proven by blob equal to HEAD and an empty `git diff HEAD`. Final `git status` was clean. The subject resolves through relative `src` imports, so no `dist` was in the path. | leg | mutation | result | |:--|:--|:--| | A1 | credit only the leaf, no hop | 16 red / 137 green: all 8 one- and two-hop pin cells, plus the join, refused and not-judgeable cases | | A2 | drop the cube leg | 11 red: all 6 cube pin cells (bare included), plus the join and cube refused cases | | A3 | cube hops through `resolveFieldPath` instead of `resolveCubeColumn` | 1 red: the declared-join case | | A4 | refused paths recorded as reads | 5 red: every refused-path case | | A5 | rename one pin builder key | 1 red: the spec census equality | **Gates.** - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at `6ae7c521fe` derived 62 commands, the same list the seat derived at dispatch. All 62 exited 0. `--ran`: "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN". - `check-widening-tells --declaration no --diff` exited 0, with "0 judged against a declared surface, 4 NOT MEASURED": no declared surface covers `packages/lint` or `.changeset`. So `Clause-②: no` rests on this reading, not on that tool: no spec key, no enum arm, no barrel export, no registration. **eslint, narrowed and proven.** - Population: the 3 changed `.ts` files. `eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, and the changeset `.md` is ignored by it. - `npx eslint --no-inline-config --format json` on those 3 files: 3 files, 0 errors, 0 warnings. - The config sets no `parserOptions.project` (no type-aware linting), so this diff cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is left to CI. ## Acceptance notes - **Observation, not filed.** `include` entries and cube `joins` keys are not in this card's slot list, and they are unchanged. A bare `include: ['account']` is credited by the general walk as a read of the base lookup. A dotted `include: ['account.region']` credits nothing on its own. The pin uses the dotted form, so the member's own credit of each lookup is what it measures. No carrier. - **Observation, not filed.** `resolveCubeColumn` answers `ok` for a member path through a join keyed by an alias that is no field on the base object. `CubeJoinSchema.name` describes such a join as one that "joins on a column the base object does not have, so nothing resolves". This rule does not take that `ok` on trust: it also resolves the hop prefix on the base object, finds no field, and records the leaf as a carrier. The cube leg's own reading was not changed (out of scope), and this was read from code only. No carrier. - **Not merged with `origin/main`.** The 3 commits that landed since BASE touch the `service-analytics` native SQL strategy, a TSDoc block in `packages/spec/src/data/field-scale.ts` and the spec liveness README. None of them touches `packages/lint`, the hop resolver or the analytics schemas. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 713b0fa commit 6210f88

4 files changed

Lines changed: 417 additions & 8 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
`field-no-consumers` no longer calls a field "inert" when a dataset or cube member reads it through a relationship path (#21439).
6+
7+
Clause-②: no
8+
9+
`os validate`, `os build` and `os lint` warned "Verdict: inert — no site of any kind names it" for every field an analytics member reached through a path such as `account.revenue`, so an author following the warning would delete a column a measure reads. The four slots that name a column are a dataset dimension's and measure's `field` and a cube dimension's and measure's `sql`. Each one now credits every field its path reads: the lookup on the base object, each intermediate lookup, and the column on the object the last hop reaches.
10+
11+
- **Hops resolve the way the analytics door resolves them.** A cube hop goes through the join the cube declares for it, else the lookup's `reference`. A dataset hop goes through the `reference` its compiler joins through, and only where the dataset's `include` declares the join.
12+
- **A bare cube column is credited too.** Before, a cube member's `sql: 'amount'` credited nothing, because a cube names its object in its own `sql`.
13+
- **A path the door refuses reads nothing.** Examples: a join the dataset's `include` does not declare, a hop that names no relationship, a column the last object does not have. Each field such a path names is now listed as a carrier site that a removal must clean (`carrier-only`), not as a reader.
14+
- **A path the object graph cannot judge** credits the fields it does resolve. An example is a lookup to an object this stack does not define.
15+
16+
Nothing new is refused, and the rule stays a warning. One warning can appear where there was none: a path the door refuses through a lookup named after its target object (`account.revenue`, with `account` a lookup to the object `account`). The old text scan credited its column as read. It is now reported `carrier-only`, beside the error the refused path already carries.

‎packages/lint/src/validate-dataset-measure-aggregates.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,14 @@ function refusedMeasureFinding(
465465
* ⛔ No hop is walked here: this only picks the object the shared walk starts
466466
* on. A bare column and the row wildcard `'*'` have no hop, and `'*'` resolves
467467
* to nothing.
468+
*
469+
* [#21439] Exported for `field-no-consumers` (`validate-field-consumers.ts`),
470+
* which credits every field a cube member's path reads by asking this
471+
* function about each prefix of the path — so a cube hop is resolved one way
472+
* in this package, and that is the door's way. Not re-exported from the
473+
* package barrel.
468474
*/
469-
function resolveCubeColumn(
475+
export function resolveCubeColumn(
470476
graph: ObjectGraph,
471477
cube: AnyRec,
472478
baseObject: string,

‎packages/lint/src/validate-field-consumers.test.ts‎

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { describe, expect, it } from 'vitest';
4+
import { z } from 'zod';
5+
import { CubeSchema } from '@objectstack/spec/data';
6+
import { DatasetSchema } from '@objectstack/spec/ui';
47
import {
58
CARRIER_ROOTS,
69
CONSUMER_ROOTS,
@@ -1263,3 +1266,223 @@ describe('[#21091] validateFieldConsumers — an inline collection reads its joi
12631266
});
12641267
});
12651268
});
1269+
1270+
describe('[#21439] validateFieldConsumers — an analytics member credits every field its column path reads', () => {
1271+
/**
1272+
* `fx_ledger.account` → `fx_account`, `fx_account.region` → `fx_region`: each
1273+
* lookup is named apart from its target, so a hop resolves through the
1274+
* relationship's `reference` and never through an object that happens to
1275+
* share its name. `name` is each object's title field, so it is exempt.
1276+
*/
1277+
const OBJECTS = [
1278+
{ name: 'fx_ledger', fields: { name: { type: 'text' }, amount: { type: 'currency' }, account: { type: 'lookup', reference: 'fx_account' } } },
1279+
{ name: 'fx_account', fields: { name: { type: 'text' }, revenue: { type: 'currency' }, region: { type: 'lookup', reference: 'fx_region' } } },
1280+
{ name: 'fx_region', fields: { name: { type: 'text' }, zone: { type: 'number' } } },
1281+
];
1282+
/** Every field the rule judges on {@link OBJECTS}, before anything reads one. */
1283+
const JUDGED = [
1284+
'fx_ledger.amount', 'fx_ledger.account', 'fx_account.revenue', 'fx_account.region', 'fx_region.zone',
1285+
];
1286+
1287+
/** `object.field` → verdict, for every field the rule reports. */
1288+
const verdicts = (s: AnyRec): Record<string, string> =>
1289+
Object.fromEntries(validateFieldConsumers(s).map((f) => [`${f.object}.${f.field}`, f.verdict]));
1290+
const reportedOf = (s: AnyRec): string[] => Object.keys(verdicts(s)).sort();
1291+
const inertExcept = (read: readonly string[]): string[] => JUDGED.filter((k) => !read.includes(k)).sort();
1292+
1293+
/**
1294+
* The `include` every pin dataset declares: `account.region` declares both
1295+
* joins (ADR-0021: "a.b" implicitly includes "a"), and as a dotted path it
1296+
* names no field the general walk credits — the bare-column cell is the
1297+
* control that shows it credits nothing on its own. (A bare entry,
1298+
* `include: ['account']`, is a field name the general walk has always
1299+
* credited, which would hide the member's own credit of the lookup.)
1300+
*/
1301+
const INCLUDE = ['account.region'];
1302+
1303+
/**
1304+
* One builder per analytics slot that names a column, keyed by where the spec
1305+
* declares it — the census below holds this key set equal to the spec's.
1306+
*/
1307+
const SLOTS: Record<string, (column: string) => AnyRec> = {
1308+
'datasets › dimensions › field': (column) => ({
1309+
datasets: [{ name: 'ds', object: 'fx_ledger', include: INCLUDE, dimensions: [{ name: 'dim', field: column, type: 'number' }], measures: [] }],
1310+
}),
1311+
'datasets › measures › field': (column) => ({
1312+
datasets: [{ name: 'ds', object: 'fx_ledger', include: INCLUDE, dimensions: [], measures: [{ name: 'mea', aggregate: 'max', field: column }] }],
1313+
}),
1314+
'analyticsCubes › dimensions › sql': (column) => ({
1315+
analyticsCubes: [{ name: 'cube', sql: 'fx_ledger', dimensions: { dim: { label: 'Dim', type: 'number', sql: column } }, measures: {} }],
1316+
}),
1317+
'analyticsCubes › measures › sql': (column) => ({
1318+
analyticsCubes: [{ name: 'cube', sql: 'fx_ledger', dimensions: {}, measures: { mea: { label: 'Mea', type: 'max', sql: column } } }],
1319+
}),
1320+
};
1321+
1322+
/** Each shape a column takes, with every field the analytics door reads for it. */
1323+
const SHAPES: [string, string, string[]][] = [
1324+
['a bare column', 'amount', ['fx_ledger.amount']],
1325+
['a one-hop path', 'account.revenue', ['fx_ledger.account', 'fx_account.revenue']],
1326+
['a two-hop path', 'account.region.zone', ['fx_ledger.account', 'fx_account.region', 'fx_region.zone']],
1327+
];
1328+
1329+
/**
1330+
* The column slots the spec declares on the two analytics roots, read off its
1331+
* own schemas: every string whose pattern is the analytics column path (the
1332+
* `[A-Za-z_]…(?:\.…)*` path half both slot patterns share). A slot the spec
1333+
* adds there lands in this list and fails the equality below until it has a
1334+
* builder — and a reader in `creditAnalyticsColumns`.
1335+
*/
1336+
const specColumnSlots = (): string[] => {
1337+
const COLUMN_PATH_SOURCE = /\[A-Za-z_\]\[A-Za-z0-9_\]\*\(\?:\\\.\[A-Za-z_\]\[A-Za-z0-9_\]\*\)\*/;
1338+
const out: string[] = [];
1339+
const walk = (doc: AnyRec, node: unknown, trail: readonly string[], seen: Set<unknown>): void => {
1340+
if (!node || typeof node !== 'object' || seen.has(node)) return;
1341+
seen.add(node);
1342+
const rec = node as AnyRec;
1343+
if (typeof rec.pattern === 'string' && COLUMN_PATH_SOURCE.test(rec.pattern)) out.push(trail.join(' › '));
1344+
// A reused shape is emitted once under `$defs` and pointed at.
1345+
if (typeof rec.$ref === 'string' && rec.$ref.startsWith('#/$defs/')) {
1346+
walk(doc, (doc.$defs as AnyRec | undefined)?.[rec.$ref.slice('#/$defs/'.length)], trail, seen);
1347+
}
1348+
// Only `properties` names an authored key; every other edge keeps the trail.
1349+
for (const [key, value] of Object.entries((rec.properties as AnyRec | undefined) ?? {})) {
1350+
walk(doc, value, [...trail, key], new Set(seen));
1351+
}
1352+
for (const edge of ['items', 'additionalProperties']) walk(doc, rec[edge], trail, seen);
1353+
for (const edge of ['anyOf', 'oneOf', 'allOf']) {
1354+
if (Array.isArray(rec[edge])) for (const arm of rec[edge] as unknown[]) walk(doc, arm, trail, seen);
1355+
}
1356+
};
1357+
for (const [root, schema] of [['analyticsCubes', CubeSchema], ['datasets', DatasetSchema]] as const) {
1358+
const doc = z.toJSONSchema(schema as unknown as z.ZodType, { io: 'input', unrepresentable: 'any' }) as AnyRec;
1359+
walk(doc, doc, [root], new Set());
1360+
}
1361+
return [...new Set(out)].sort();
1362+
};
1363+
1364+
it('the pin covers every column slot the spec declares on a dataset and a cube', () => {
1365+
const slots = specColumnSlots();
1366+
expect(slots.length).toBeGreaterThanOrEqual(4);
1367+
expect(slots).toEqual(Object.keys(SLOTS).sort());
1368+
});
1369+
1370+
it('baseline: with no analytics member reading them, every judged field is inert', () => {
1371+
expect(verdicts({ objects: OBJECTS, views: [{ list: { data: { object: 'fx_ledger' }, columns: [] } }] })).toEqual(
1372+
Object.fromEntries(JUDGED.map((k) => [k, 'inert'])),
1373+
);
1374+
});
1375+
1376+
describe('the enumeration pin: every slot × every shape credits each field the path reads', () => {
1377+
const cells = Object.entries(SLOTS).flatMap(([slot, build]) =>
1378+
SHAPES.map(([shape, column, read]): [string, string, AnyRec, string[]] => [slot, shape, { objects: OBJECTS, ...build(column) }, read]),
1379+
);
1380+
1381+
it('pins 4 slots × 3 shapes', () => {
1382+
expect(cells).toHaveLength(12);
1383+
});
1384+
1385+
it.each(cells)('%s × %s: exactly the fields it reads leave the report', (_slot, _shape, s, read) => {
1386+
expect(reportedOf(s)).toEqual(inertExcept(read));
1387+
});
1388+
});
1389+
1390+
describe('a cube hop resolves through its declared join first, else the reference', () => {
1391+
const ALT = { name: 'fx_account_alt', fields: { name: { type: 'text' }, revenue: { type: 'currency' } } };
1392+
const cube = (joins: AnyRec | undefined): AnyRec => ({
1393+
objects: [...OBJECTS, ALT],
1394+
analyticsCubes: [{ name: 'cube', sql: 'fx_ledger', ...(joins ? { joins } : {}), dimensions: {}, measures: { mea: { label: 'M', type: 'sum', sql: 'account.revenue' } } }],
1395+
});
1396+
1397+
it('a declared join reaches its own object, and the reference target stays unread', () => {
1398+
const v = verdicts(cube({ account: { name: 'fx_account_alt' } }));
1399+
expect(v['fx_ledger.account']).toBeUndefined();
1400+
expect(v['fx_account_alt.revenue']).toBeUndefined();
1401+
expect(v['fx_account.revenue']).toBe('inert');
1402+
});
1403+
1404+
it('with no join for the hop, the reference target is read and the other object is not', () => {
1405+
const v = verdicts(cube(undefined));
1406+
expect(v['fx_account.revenue']).toBeUndefined();
1407+
expect(v['fx_account_alt.revenue']).toBe('inert');
1408+
});
1409+
});
1410+
1411+
describe('a path the door refuses names its fields as carriers, never as reads', () => {
1412+
const dataset = (field: string, include: string[]): AnyRec => ({
1413+
objects: OBJECTS,
1414+
datasets: [{ name: 'ds', object: 'fx_ledger', include, dimensions: [], measures: [{ name: 'mea', aggregate: 'max', field }] }],
1415+
});
1416+
1417+
it('a dataset path whose join `include` does not declare: every field it names is carrier-only', () => {
1418+
const findings = byPath(validateFieldConsumers(dataset('account.revenue', [])));
1419+
for (const declaration of ['objects[0].fields.account', 'objects[1].fields.revenue']) {
1420+
expect(findings[declaration].verdict).toBe('carrier-only');
1421+
expect(findings[declaration].carriers).toEqual(['datasets[0].measures[0].field']);
1422+
}
1423+
});
1424+
1425+
it('declaring the first hop does not declare the second: the two-hop path is still refused', () => {
1426+
// `account` stays read here, by the bare `include` entry the general walk
1427+
// credits — not by the refused member path.
1428+
const v = verdicts(dataset('account.region.zone', ['account']));
1429+
expect(v).toEqual({ 'fx_ledger.amount': 'inert', 'fx_account.revenue': 'inert', 'fx_account.region': 'carrier-only', 'fx_region.zone': 'carrier-only' });
1430+
});
1431+
1432+
it('declared, the same paths are read', () => {
1433+
expect(reportedOf(dataset('account.revenue', INCLUDE))).toEqual(inertExcept(['fx_ledger.account', 'fx_account.revenue']));
1434+
expect(reportedOf(dataset('account.region.zone', INCLUDE))).toEqual(
1435+
inertExcept(['fx_ledger.account', 'fx_account.region', 'fx_region.zone']),
1436+
);
1437+
});
1438+
1439+
it.each([
1440+
['a column the last hop does not declare', 'account.revnue', { 'fx_ledger.account': 'carrier-only', 'fx_account.revenue': 'inert' }],
1441+
['a hop that names no field', 'acount.revenue', { 'fx_ledger.account': 'inert', 'fx_account.revenue': 'inert' }],
1442+
['a hop through a field that is not a relationship', 'amount.revenue', { 'fx_ledger.amount': 'carrier-only', 'fx_account.revenue': 'inert' }],
1443+
])('a cube member through %s', (_label, sql, expected) => {
1444+
const v = verdicts({
1445+
objects: OBJECTS,
1446+
analyticsCubes: [{ name: 'cube', sql: 'fx_ledger', dimensions: { dim: { label: 'D', type: 'string', sql } }, measures: {} }],
1447+
});
1448+
expect(v).toMatchObject(expected);
1449+
});
1450+
1451+
it('a lookup named like its target object gains no read from a refused path (the slot is not text-scanned)', () => {
1452+
const v = verdicts({
1453+
objects: [
1454+
{ name: 'fx_ledger', fields: { name: { type: 'text' }, fx_account: { type: 'lookup', reference: 'fx_account' } } },
1455+
{ name: 'fx_account', fields: { name: { type: 'text' }, revenue: { type: 'currency' } } },
1456+
],
1457+
datasets: [{ name: 'ds', object: 'fx_ledger', dimensions: [], measures: [{ name: 'mea', aggregate: 'max', field: 'fx_account.revenue' }] }],
1458+
});
1459+
expect(v).toEqual({ 'fx_ledger.fx_account': 'carrier-only', 'fx_account.revenue': 'carrier-only' });
1460+
});
1461+
});
1462+
1463+
describe('a path the graph cannot judge credits the fields it resolves', () => {
1464+
it('a hop to an object this stack does not define: the lookup on the base object is read', () => {
1465+
const v = verdicts({
1466+
objects: [{ name: 'fx_ledger', fields: { name: { type: 'text' }, manager: { type: 'lookup', reference: 'ext_person' } } }],
1467+
analyticsCubes: [{ name: 'cube', sql: 'fx_ledger', dimensions: { dim: { label: 'D', type: 'string', sql: 'manager.email' } }, measures: {} }],
1468+
});
1469+
expect(v).toEqual({});
1470+
});
1471+
1472+
it('the row wildcard reads no field and credits none', () => {
1473+
const v = verdicts({
1474+
objects: OBJECTS,
1475+
analyticsCubes: [{ name: 'cube', sql: 'fx_ledger', dimensions: {}, measures: { rows: { label: 'Rows', type: 'count', sql: '*' } } }],
1476+
});
1477+
expect(Object.keys(v).sort()).toEqual([...JUDGED].sort());
1478+
});
1479+
1480+
it('a base object this stack does not define leaves the slot to the general walk, as before', () => {
1481+
const v = verdicts({
1482+
objects: OBJECTS,
1483+
analyticsCubes: [{ name: 'cube', sql: 'ext_ledger', dimensions: { dim: { label: 'D', type: 'number', sql: 'account.revenue' } }, measures: {} }],
1484+
});
1485+
expect(Object.keys(v).sort()).toEqual([...JUDGED].sort());
1486+
});
1487+
});
1488+
});

0 commit comments

Comments
 (0)