Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/analytics-current-user-token.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
'@objectstack/service-analytics': minor
---

Resolve `{current_user_id}` (and every other filter placeholder) on the direct analytics query path, at parity with the list path and the dashboard dataset path.

What changes for an app author: a widget or report whose filter says `owner: '{current_user_id}'` used to render `0` for every viewer whenever the query reached the SQL strategy — the literal text was bound into the `WHERE` and matched no row, silently. Now the same filter expression means the same thing on every surface: `AnalyticsService.query` and `generateSql` expand `where`, `timeDimensions[].dateRange`, and a registered dataset's own filter / measure filters against the requesting user before any strategy compiles, so each viewer gets their own rows. A placeholder that cannot be resolved — an unknown spelling, or `{current_user_id}` on an unauthenticated request — now refuses loudly with `FILTER_TOKEN_UNKNOWN` / `FILTER_TOKEN_UNRESOLVED` (HTTP 400) instead of charting a plausible zero.

This also closes a gap on the dashboard dataset door: the dataset-scope channel used to hand strategies the registry's unresolved filter copy, which was ANDed in beside the resolved one (`owner = $viewer AND owner = '{current_user_id}'`) and selected nothing.
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,12 @@ describe('dataset filter placeholders (framework#3582)', () => {
);

expect(captured[0].params).toContain('usr_1');
// [#12230] The absence half is the load-bearing assertion: the #10298
// dataset-scope conjunct used to AND the REGISTRY's unresolved copy in
// beside the executor's resolved one — `owner = $viewer AND
// owner = '{current_user_id}'` binds both params and selects nothing,
// and `toContain('usr_1')` alone stayed green through it.
expect(captured[0].params).not.toContain('{current_user_id}');
});

it('expands a measure-scoped filter', async () => {
Expand All@@ -104,6 +110,11 @@ describe('dataset filter placeholders (framework#3582)', () => {
);

expect(captured.some((c) => c.params.includes(THIS_YEAR_START))).toBe(true);
// [#12230] Same absence pin for the measure-filter channel: the strategy's
// conditional aggregate (`CASE WHEN`) used to compile the registry's
// unresolved measure filter, zeroing the measure while the resolved copy
// in `where` kept this presence assertion green.
expect(captured.every((c) => !c.params.includes('{current_year_start}'))).toBe(true);
});

it('never mutates the registered dataset — it is reused across requests', async () => {
Expand All@@ -125,6 +136,7 @@ describe('dataset filter placeholders (framework#3582)', () => {
// Second render must scope to the SECOND user, not a baked-in first one.
expect(captured[1].params).toContain('usr_2');
expect(captured[1].params).not.toContain('usr_1');
expect(captured[1].params).not.toContain('{current_user_id}');
expect(scoped.filter).toEqual({ owner: '{current_user_id}' });
});

Expand Down
Loading
Loading