@opensaas/stack-core 0.30.0 → 0.31.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +410 -0
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +14 -1
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +50 -0
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/field-access.js +6 -6
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/multi-column-read-write.test.js +46 -12
- package/dist/access/multi-column-read-write.test.js.map +1 -1
- package/dist/access/relationship-count.d.ts +60 -0
- package/dist/access/relationship-count.d.ts.map +1 -0
- package/dist/access/relationship-count.js +233 -0
- package/dist/access/relationship-count.js.map +1 -0
- package/dist/access/relationship-count.test.d.ts +2 -0
- package/dist/access/relationship-count.test.d.ts.map +1 -0
- package/dist/access/relationship-count.test.js +185 -0
- package/dist/access/relationship-count.test.js.map +1 -0
- package/dist/access/relationship-label-filter.d.ts +45 -0
- package/dist/access/relationship-label-filter.d.ts.map +1 -0
- package/dist/access/relationship-label-filter.js +93 -0
- package/dist/access/relationship-label-filter.js.map +1 -0
- package/dist/access/relationship-label-filter.test.d.ts +2 -0
- package/dist/access/relationship-label-filter.test.d.ts.map +1 -0
- package/dist/access/relationship-label-filter.test.js +126 -0
- package/dist/access/relationship-label-filter.test.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/nav-count.d.ts +34 -0
- package/dist/config/nav-count.d.ts.map +1 -0
- package/dist/config/nav-count.js +71 -0
- package/dist/config/nav-count.js.map +1 -0
- package/dist/config/types.d.ts +305 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +11 -1
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +28 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +205 -0
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +7 -1
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/extend.d.ts +1 -0
- package/dist/extend.d.ts.map +1 -1
- package/dist/fields/index.d.ts +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +166 -0
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +32 -0
- package/dist/filter/collect.d.ts.map +1 -0
- package/dist/filter/collect.js +60 -0
- package/dist/filter/collect.js.map +1 -0
- package/dist/filter/filter.test.d.ts +2 -0
- package/dist/filter/filter.test.d.ts.map +1 -0
- package/dist/filter/filter.test.js +348 -0
- package/dist/filter/filter.test.js.map +1 -0
- package/dist/filter/index.d.ts +7 -0
- package/dist/filter/index.d.ts.map +1 -0
- package/dist/filter/index.js +16 -0
- package/dist/filter/index.js.map +1 -0
- package/dist/filter/map.d.ts +18 -0
- package/dist/filter/map.d.ts.map +1 -0
- package/dist/filter/map.js +60 -0
- package/dist/filter/map.js.map +1 -0
- package/dist/filter/parse.d.ts +33 -0
- package/dist/filter/parse.d.ts.map +1 -0
- package/dist/filter/parse.js +103 -0
- package/dist/filter/parse.js.map +1 -0
- package/dist/filter/serialize.d.ts +25 -0
- package/dist/filter/serialize.d.ts.map +1 -0
- package/dist/filter/serialize.js +68 -0
- package/dist/filter/serialize.js.map +1 -0
- package/dist/filter/serialize.test.d.ts +2 -0
- package/dist/filter/serialize.test.d.ts.map +1 -0
- package/dist/filter/serialize.test.js +83 -0
- package/dist/filter/serialize.test.js.map +1 -0
- package/dist/filter/types.d.ts +122 -0
- package/dist/filter/types.d.ts.map +1 -0
- package/dist/filter/types.js +24 -0
- package/dist/filter/types.js.map +1 -0
- package/dist/hooks/index.d.ts +30 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +79 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +79 -0
- package/src/access/access-filter.ts +15 -4
- package/src/access/field-access.ts +6 -6
- package/src/access/multi-column-read-write.test.ts +68 -19
- package/src/access/relationship-count.test.ts +269 -0
- package/src/access/relationship-count.ts +311 -0
- package/src/access/relationship-label-filter.test.ts +177 -0
- package/src/access/relationship-label-filter.ts +140 -0
- package/src/config/index.ts +6 -0
- package/src/config/nav-count.ts +85 -0
- package/src/config/types.ts +319 -1
- package/src/context/hook-pipeline.ts +19 -0
- package/src/context/index.ts +285 -3
- package/src/context/nested-operations.ts +22 -0
- package/src/extend.ts +9 -0
- package/src/fields/index.ts +168 -0
- package/src/filter/collect.ts +72 -0
- package/src/filter/filter.test.ts +400 -0
- package/src/filter/index.ts +25 -0
- package/src/filter/map.ts +68 -0
- package/src/filter/parse.ts +106 -0
- package/src/filter/serialize.test.ts +115 -0
- package/src/filter/serialize.ts +75 -0
- package/src/filter/types.ts +120 -0
- package/src/hooks/index.ts +87 -57
- package/src/index.ts +54 -0
- package/src/internal.ts +5 -0
- package/tests/context.test.ts +690 -0
- package/tests/multi-column-validation-ordering.test.ts +305 -0
- package/tests/nav-count.test.ts +189 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { list } from '../config/index.js';
|
|
3
|
+
import { text, relationship, integer } from '../fields/index.js';
|
|
4
|
+
import { buildRelationshipCountSelect, resolveRelationshipCountFilters, isToManyRelationshipField, } from './relationship-count.js';
|
|
5
|
+
import { RELATIONSHIP_COUNT_FILTER_KEY } from '../filter/types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Access-scoped to-many relationship counts (issue #732). Verifies:
|
|
8
|
+
* • the filtered `_count` select folds in the related list's query access,
|
|
9
|
+
* • a denied related list is omitted (its count renders as 0, never a leak),
|
|
10
|
+
* • count-filter markers resolve to access-scoped `{ id: { in } }` via a single
|
|
11
|
+
* secured read (never a per-row query), including the fully-denied case.
|
|
12
|
+
*/
|
|
13
|
+
// User (published-only for anon via a filter), plus a Widget list that is fully
|
|
14
|
+
// closed by default (no query access) to prove the denied path.
|
|
15
|
+
function makeConfig() {
|
|
16
|
+
return {
|
|
17
|
+
db: { provider: 'sqlite', prismaClientConstructor: () => null },
|
|
18
|
+
lists: {
|
|
19
|
+
User: list({
|
|
20
|
+
fields: {
|
|
21
|
+
name: text(),
|
|
22
|
+
posts: relationship({ ref: 'Post.author', many: true }),
|
|
23
|
+
widgets: relationship({ ref: 'Widget', many: true }),
|
|
24
|
+
manager: relationship({ ref: 'User' }), // to-one — never counted
|
|
25
|
+
},
|
|
26
|
+
access: { operation: { query: () => true } },
|
|
27
|
+
}),
|
|
28
|
+
Post: list({
|
|
29
|
+
fields: { title: text(), views: integer(), author: relationship({ ref: 'User.posts' }) },
|
|
30
|
+
// Only published posts are visible → the count must be access-scoped.
|
|
31
|
+
access: { operation: { query: () => ({ status: { equals: 'published' } }) } },
|
|
32
|
+
}),
|
|
33
|
+
// Widget ships closed (no access block) → query denied by default.
|
|
34
|
+
Widget: list({ fields: { name: text() } }),
|
|
35
|
+
},
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function makeContext(findMany) {
|
|
40
|
+
return {
|
|
41
|
+
session: null,
|
|
42
|
+
_isSudo: false,
|
|
43
|
+
_resolveOutputCounter: { depth: 0 },
|
|
44
|
+
db: findMany ? { user: { findMany } } : {},
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal context for unit test
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
describe('isToManyRelationshipField', () => {
|
|
49
|
+
const config = makeConfig();
|
|
50
|
+
const userFields = config.lists.User.fields;
|
|
51
|
+
it('is true only for a to-many relationship', () => {
|
|
52
|
+
expect(isToManyRelationshipField(userFields.posts)).toBe(true);
|
|
53
|
+
expect(isToManyRelationshipField(userFields.widgets)).toBe(true);
|
|
54
|
+
expect(isToManyRelationshipField(userFields.manager)).toBe(false);
|
|
55
|
+
expect(isToManyRelationshipField(userFields.name)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('buildRelationshipCountSelect', () => {
|
|
59
|
+
it("folds the related list's query filter into each to-many count and omits denied/to-one", async () => {
|
|
60
|
+
const config = makeConfig();
|
|
61
|
+
const select = await buildRelationshipCountSelect(config.lists.User, { session: null, context: makeContext() }, config);
|
|
62
|
+
// `posts` → access-scoped where; `widgets` → denied (omitted); `manager` →
|
|
63
|
+
// to-one (never counted).
|
|
64
|
+
expect(select).toEqual({ posts: { where: { status: { equals: 'published' } } } });
|
|
65
|
+
});
|
|
66
|
+
it('uses a bare `true` when the related list is fully readable', async () => {
|
|
67
|
+
const config = {
|
|
68
|
+
db: { provider: 'sqlite', prismaClientConstructor: () => null },
|
|
69
|
+
lists: {
|
|
70
|
+
Tag: list({ fields: { name: text() }, access: { operation: { query: () => true } } }),
|
|
71
|
+
Post: list({ fields: { title: text(), tags: relationship({ ref: 'Tag', many: true }) } }),
|
|
72
|
+
},
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
74
|
+
};
|
|
75
|
+
const select = await buildRelationshipCountSelect(config.lists.Post, { session: null, context: makeContext() }, config);
|
|
76
|
+
expect(select).toEqual({ tags: true });
|
|
77
|
+
});
|
|
78
|
+
it('returns undefined when there are no countable to-many relationships', async () => {
|
|
79
|
+
const config = {
|
|
80
|
+
db: { provider: 'sqlite', prismaClientConstructor: () => null },
|
|
81
|
+
lists: { Post: list({ fields: { title: text() } }) },
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
83
|
+
};
|
|
84
|
+
const select = await buildRelationshipCountSelect(config.lists.Post, { session: null, context: makeContext() }, config);
|
|
85
|
+
expect(select).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('resolveRelationshipCountFilters', () => {
|
|
89
|
+
const marker = (operator, value) => ({
|
|
90
|
+
posts: { [RELATIONSHIP_COUNT_FILTER_KEY]: { operator, value } },
|
|
91
|
+
});
|
|
92
|
+
it('returns the where unchanged when there are no count markers', async () => {
|
|
93
|
+
const config = makeConfig();
|
|
94
|
+
const where = { name: { contains: 'ada' } };
|
|
95
|
+
const resolved = await resolveRelationshipCountFilters(where, config.lists.User, 'User', { session: null, context: makeContext() }, config);
|
|
96
|
+
expect(resolved).toBe(where);
|
|
97
|
+
});
|
|
98
|
+
it('resolves a count marker to an access-scoped { id: { in } } via a single secured read', async () => {
|
|
99
|
+
const config = makeConfig();
|
|
100
|
+
// Two users; the secured read returns each with its access-scoped `_count`.
|
|
101
|
+
const findMany = vi.fn(async () => [
|
|
102
|
+
{ id: 'u1', _count: { posts: 3 } },
|
|
103
|
+
{ id: 'u2', _count: { posts: 7 } },
|
|
104
|
+
]);
|
|
105
|
+
const resolved = await resolveRelationshipCountFilters(marker('gt', 5), config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
106
|
+
// Only u2 (7 > 5) matches; the read was access-scoped (published-only count).
|
|
107
|
+
expect(resolved).toEqual({ id: { in: ['u2'] } });
|
|
108
|
+
expect(findMany).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(findMany).toHaveBeenCalledWith({
|
|
110
|
+
include: { _count: { select: { posts: { where: { status: { equals: 'published' } } } } } },
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
it('includes zero-count rows for comparisons that admit zero (no complement bug)', async () => {
|
|
114
|
+
const config = makeConfig();
|
|
115
|
+
const findMany = vi.fn(async () => [
|
|
116
|
+
{ id: 'u1', _count: { posts: 0 } },
|
|
117
|
+
{ id: 'u2', _count: { posts: 4 } },
|
|
118
|
+
]);
|
|
119
|
+
const resolved = await resolveRelationshipCountFilters(marker('lt', 1), // count < 1 → only the zero-count user
|
|
120
|
+
config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
121
|
+
expect(resolved).toEqual({ id: { in: ['u1'] } });
|
|
122
|
+
});
|
|
123
|
+
it('resolves a denied related list without any query: count is always 0', async () => {
|
|
124
|
+
const config = makeConfig();
|
|
125
|
+
const findMany = vi.fn(async () => []);
|
|
126
|
+
// `widgets`' related list (Widget) is fully denied → every count is 0.
|
|
127
|
+
const where = { widgets: { [RELATIONSHIP_COUNT_FILTER_KEY]: { operator: 'gt', value: 5 } } };
|
|
128
|
+
const resolved = await resolveRelationshipCountFilters(where, config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
129
|
+
// 0 is not > 5 → nothing matches, and no read was issued.
|
|
130
|
+
expect(resolved).toEqual({ id: { in: [] } });
|
|
131
|
+
expect(findMany).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
it('preserves sibling conditions, ANDing the resolved id constraint', async () => {
|
|
134
|
+
const config = makeConfig();
|
|
135
|
+
const findMany = vi.fn(async () => [{ id: 'u2', _count: { posts: 7 } }]);
|
|
136
|
+
const where = { AND: [{ name: { contains: 'ada' } }, marker('gte', 1)] };
|
|
137
|
+
const resolved = await resolveRelationshipCountFilters(where, config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
138
|
+
expect(resolved).toEqual({ AND: [{ name: { contains: 'ada' } }, { id: { in: ['u2'] } }] });
|
|
139
|
+
});
|
|
140
|
+
it('reads with only the filtered `_count` include — no over-fetching `select` projection', async () => {
|
|
141
|
+
const config = makeConfig();
|
|
142
|
+
const findMany = vi.fn((_args) => Promise.resolve([{ id: 'u2', _count: { posts: 7 } }]));
|
|
143
|
+
await resolveRelationshipCountFilters(marker('gt', 5), config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
144
|
+
// The secured read is issued with the access-scoped `_count` include and NO
|
|
145
|
+
// `select` — the secured findMany does not honour `select`, so forcing one
|
|
146
|
+
// would be an ignored no-op. Access-scoping lives entirely in `_count.where`.
|
|
147
|
+
expect(findMany).toHaveBeenCalledTimes(1);
|
|
148
|
+
const callArg = findMany.mock.calls[0][0];
|
|
149
|
+
expect(callArg).not.toHaveProperty('select');
|
|
150
|
+
expect(callArg).toEqual({
|
|
151
|
+
include: { _count: { select: { posts: { where: { status: { equals: 'published' } } } } } },
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
it('preserves a sibling condition co-present in the same member as the marker', async () => {
|
|
155
|
+
const config = makeConfig();
|
|
156
|
+
const findMany = vi.fn(async () => [{ id: 'u2', _count: { posts: 7 } }]);
|
|
157
|
+
// A single AND-member carrying BOTH a scalar condition and the count marker.
|
|
158
|
+
// The marker resolution must keep the co-present sibling, not replace the
|
|
159
|
+
// member wholesale (guards against a future filter-engine change that merges
|
|
160
|
+
// conditions into one member).
|
|
161
|
+
const where = {
|
|
162
|
+
AND: [
|
|
163
|
+
{
|
|
164
|
+
name: { contains: 'ada' },
|
|
165
|
+
posts: { [RELATIONSHIP_COUNT_FILTER_KEY]: { operator: 'gt', value: 5 } },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
const resolved = await resolveRelationshipCountFilters(where, config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
170
|
+
expect(resolved).toEqual({ name: { contains: 'ada' }, id: { in: ['u2'] } });
|
|
171
|
+
});
|
|
172
|
+
it('ANDs the resolved id constraint with a co-present sibling id condition (no silent drop)', async () => {
|
|
173
|
+
const config = makeConfig();
|
|
174
|
+
const findMany = vi.fn(async () => [{ id: 'u2', _count: { posts: 7 } }]);
|
|
175
|
+
// Contrived: a member carrying its own `id` condition alongside the marker.
|
|
176
|
+
// Spreading would let one `id` overwrite the other; instead both are ANDed.
|
|
177
|
+
const where = {
|
|
178
|
+
id: { in: ['u1', 'u2'] },
|
|
179
|
+
posts: { [RELATIONSHIP_COUNT_FILTER_KEY]: { operator: 'gt', value: 5 } },
|
|
180
|
+
};
|
|
181
|
+
const resolved = await resolveRelationshipCountFilters(where, config.lists.User, 'User', { session: null, context: makeContext(findMany) }, config);
|
|
182
|
+
expect(resolved).toEqual({ AND: [{ id: { in: ['u1', 'u2'] } }, { id: { in: ['u2'] } }] });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=relationship-count.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-count.test.js","sourceRoot":"","sources":["../../src/access/relationship-count.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Session, AccessContext } from './types.js';
|
|
2
|
+
import type { OpenSaasConfig, ListConfig, FieldConfig } from '../config/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Access-scoped to-one relationship label filters for the admin list view
|
|
5
|
+
* (issue #749).
|
|
6
|
+
*
|
|
7
|
+
* A to-one relationship's Filter spec (`author:Ada` → `{ author: { is: { name:
|
|
8
|
+
* { contains: 'Ada' } } } }`) is a pure mapper (see `relationship()` in
|
|
9
|
+
* `fields/index.ts`) — it has no way to consult the related list's access
|
|
10
|
+
* control, so the nested `is` clause it emits runs as an unscoped Prisma
|
|
11
|
+
* sub-filter against the related table. The parent list's own access filter
|
|
12
|
+
* still scopes which parent rows are visible, but the nested condition itself
|
|
13
|
+
* is evaluated with no reference to the related list's `query` access — a
|
|
14
|
+
* session could distinguish parent rows by a related field it is not itself
|
|
15
|
+
* allowed to read (e.g. binary-searching `author:A`, `author:Ad`, `author:Ada`
|
|
16
|
+
* against a `User` list it cannot query).
|
|
17
|
+
*
|
|
18
|
+
* This module is the single place the related list's operation-level `query`
|
|
19
|
+
* access is folded into that nested `is` clause, mirroring how
|
|
20
|
+
* `relationship-count.ts` folds it into `_count` selects and count-filter
|
|
21
|
+
* markers. A fully denied related list makes the member never match — the
|
|
22
|
+
* token cannot be used to confirm or rule out a value on a field the session
|
|
23
|
+
* cannot read (narrowing-only, matching the count resolver's denied path).
|
|
24
|
+
*/
|
|
25
|
+
type LabelFilterArgs = {
|
|
26
|
+
session: Session | null;
|
|
27
|
+
context: AccessContext;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Whether a field is a to-one relationship — the only field kind whose Filter
|
|
31
|
+
* spec emits a nested `{ is: {...} } }` condition against the related list's
|
|
32
|
+
* label field.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isToOneRelationshipField(field: FieldConfig | undefined): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Replace any to-one relationship label-filter members in a filter `where`
|
|
37
|
+
* with access-scoped equivalents. Label-filter members only ever appear as
|
|
38
|
+
* top-level AND members (the relationship Filter spec does not declare
|
|
39
|
+
* `freeText`, so it never participates in the free-text OR), so this walks
|
|
40
|
+
* only the top level. Returns the `where` unchanged when it contains no such
|
|
41
|
+
* members, so lists without to-one relationship filters pay nothing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveRelationshipLabelFilters(where: Record<string, unknown> | undefined, listConfig: ListConfig<any>, args: LabelFilterArgs, config: OpenSaasConfig): Promise<Record<string, unknown> | undefined>;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=relationship-label-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-label-filter.d.ts","sourceRoot":"","sources":["../../src/access/relationship-label-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIjF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,KAAK,eAAe,GAAG;IACrB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,CAAA;CACvB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAQhF;AAsCD;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAE1C,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,EAC3B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAyC9C"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { checkAccess, getRelatedListConfig } from './engine.js';
|
|
2
|
+
import { mergeResolvedMember } from './relationship-count.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a field is a to-one relationship — the only field kind whose Filter
|
|
5
|
+
* spec emits a nested `{ is: {...} } }` condition against the related list's
|
|
6
|
+
* label field.
|
|
7
|
+
*/
|
|
8
|
+
export function isToOneRelationshipField(field) {
|
|
9
|
+
return (field?.type === 'relationship' &&
|
|
10
|
+
!('many' in field && field.many === true) &&
|
|
11
|
+
'ref' in field &&
|
|
12
|
+
typeof field.ref === 'string' &&
|
|
13
|
+
field.ref.length > 0);
|
|
14
|
+
}
|
|
15
|
+
/** Narrow a filter member's field value to a `{ is: {...} } }` clause, if it is one. */
|
|
16
|
+
function readIsClause(value) {
|
|
17
|
+
if (!value || typeof value !== 'object')
|
|
18
|
+
return null;
|
|
19
|
+
const isValue = value.is;
|
|
20
|
+
if (!isValue || typeof isValue !== 'object')
|
|
21
|
+
return null;
|
|
22
|
+
return isValue;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve one to-one relationship label-filter member into an access-scoped
|
|
26
|
+
* condition by ANDing the related list's `query` access filter into the nested
|
|
27
|
+
* `is` clause. Returns `{ id: { in: [] } }` (never matches) when the related
|
|
28
|
+
* list is fully denied, the original member unchanged when it is fully
|
|
29
|
+
* readable, and the `is` clause ANDed with the access filter otherwise.
|
|
30
|
+
*/
|
|
31
|
+
async function resolveOneLabelFilter(field, fieldName, isClause, args, config) {
|
|
32
|
+
if (!('ref' in field) || typeof field.ref !== 'string') {
|
|
33
|
+
return { [fieldName]: { is: isClause } };
|
|
34
|
+
}
|
|
35
|
+
const related = getRelatedListConfig(field.ref, config);
|
|
36
|
+
if (!related)
|
|
37
|
+
return { [fieldName]: { is: isClause } };
|
|
38
|
+
const queryAccess = related.listConfig.access?.operation?.query;
|
|
39
|
+
const result = await checkAccess(queryAccess, { session: args.session, context: args.context });
|
|
40
|
+
if (result === false)
|
|
41
|
+
return { id: { in: [] } };
|
|
42
|
+
if (result === true)
|
|
43
|
+
return { [fieldName]: { is: isClause } };
|
|
44
|
+
return { [fieldName]: { is: { AND: [result, isClause] } } };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Replace any to-one relationship label-filter members in a filter `where`
|
|
48
|
+
* with access-scoped equivalents. Label-filter members only ever appear as
|
|
49
|
+
* top-level AND members (the relationship Filter spec does not declare
|
|
50
|
+
* `freeText`, so it never participates in the free-text OR), so this walks
|
|
51
|
+
* only the top level. Returns the `where` unchanged when it contains no such
|
|
52
|
+
* members, so lists without to-one relationship filters pay nothing.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveRelationshipLabelFilters(where,
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
56
|
+
listConfig, args, config) {
|
|
57
|
+
if (!where)
|
|
58
|
+
return where;
|
|
59
|
+
const andValue = where.AND;
|
|
60
|
+
const members = Array.isArray(andValue)
|
|
61
|
+
? andValue
|
|
62
|
+
: [where];
|
|
63
|
+
const findLabelFilter = (member) => {
|
|
64
|
+
for (const key of Object.keys(member)) {
|
|
65
|
+
const field = listConfig.fields[key];
|
|
66
|
+
if (!isToOneRelationshipField(field))
|
|
67
|
+
continue;
|
|
68
|
+
const isClause = readIsClause(member[key]);
|
|
69
|
+
if (!isClause)
|
|
70
|
+
continue;
|
|
71
|
+
return { field: key, isClause };
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
if (!members.some((member) => findLabelFilter(member) !== null)) {
|
|
76
|
+
return where;
|
|
77
|
+
}
|
|
78
|
+
const resolvedMembers = [];
|
|
79
|
+
for (const member of members) {
|
|
80
|
+
const found = findLabelFilter(member);
|
|
81
|
+
if (!found) {
|
|
82
|
+
resolvedMembers.push(member);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const field = listConfig.fields[found.field];
|
|
86
|
+
const resolved = await resolveOneLabelFilter(field, found.field, found.isClause, args, config);
|
|
87
|
+
const siblings = { ...member };
|
|
88
|
+
delete siblings[found.field];
|
|
89
|
+
resolvedMembers.push(mergeResolvedMember(siblings, resolved));
|
|
90
|
+
}
|
|
91
|
+
return resolvedMembers.length === 1 ? resolvedMembers[0] : { AND: resolvedMembers };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=relationship-label-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-label-filter.js","sourceRoot":"","sources":["../../src/access/relationship-label-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-label-filter.test.d.ts","sourceRoot":"","sources":["../../src/access/relationship-label-filter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { list } from '../config/index.js';
|
|
3
|
+
import { text, relationship, integer } from '../fields/index.js';
|
|
4
|
+
import { resolveRelationshipLabelFilters, isToOneRelationshipField, } from './relationship-label-filter.js';
|
|
5
|
+
/**
|
|
6
|
+
* Access-scoped to-one relationship label filters (issue #749). Verifies that
|
|
7
|
+
* `author:Ada` → `{ author: { is: { name: { contains: 'Ada' } } } }` gets the
|
|
8
|
+
* related list's `query` access folded into the nested `is` clause, so a
|
|
9
|
+
* session can never distinguish parent rows by a related field it cannot
|
|
10
|
+
* itself read.
|
|
11
|
+
*/
|
|
12
|
+
// Post is fully open; User (the `author` relation target) is scoped so only
|
|
13
|
+
// active users are queryable; Widget ships fully closed (no access block).
|
|
14
|
+
function makeConfig() {
|
|
15
|
+
return {
|
|
16
|
+
db: { provider: 'sqlite', prismaClientConstructor: () => null },
|
|
17
|
+
lists: {
|
|
18
|
+
User: list({
|
|
19
|
+
fields: { name: text(), active: integer() },
|
|
20
|
+
access: { operation: { query: () => ({ active: { equals: 1 } }) } },
|
|
21
|
+
}),
|
|
22
|
+
Post: list({
|
|
23
|
+
fields: {
|
|
24
|
+
title: text(),
|
|
25
|
+
views: integer(),
|
|
26
|
+
author: relationship({ ref: 'User.posts' }),
|
|
27
|
+
widget: relationship({ ref: 'Widget' }),
|
|
28
|
+
},
|
|
29
|
+
access: { operation: { query: () => true } },
|
|
30
|
+
}),
|
|
31
|
+
// Widget ships closed (no access block) → query denied by default.
|
|
32
|
+
Widget: list({ fields: { name: text() } }),
|
|
33
|
+
},
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function makeContext() {
|
|
38
|
+
return {
|
|
39
|
+
session: null,
|
|
40
|
+
_isSudo: false,
|
|
41
|
+
_resolveOutputCounter: { depth: 0 },
|
|
42
|
+
db: {},
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal context for unit test
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe('isToOneRelationshipField', () => {
|
|
47
|
+
const config = makeConfig();
|
|
48
|
+
const postFields = config.lists.Post.fields;
|
|
49
|
+
it('is true only for a to-one relationship', () => {
|
|
50
|
+
expect(isToOneRelationshipField(postFields.author)).toBe(true);
|
|
51
|
+
expect(isToOneRelationshipField(postFields.widget)).toBe(true);
|
|
52
|
+
expect(isToOneRelationshipField(postFields.title)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('resolveRelationshipLabelFilters', () => {
|
|
56
|
+
it('returns the where unchanged when there are no label-filter members', async () => {
|
|
57
|
+
const config = makeConfig();
|
|
58
|
+
const where = { title: { contains: 'hello' } };
|
|
59
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
60
|
+
expect(resolved).toBe(where);
|
|
61
|
+
});
|
|
62
|
+
it("ANDs the related list's access filter into the nested `is` clause", async () => {
|
|
63
|
+
const config = makeConfig();
|
|
64
|
+
const where = { author: { is: { name: { contains: 'Ada' } } } };
|
|
65
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
66
|
+
expect(resolved).toEqual({
|
|
67
|
+
author: { is: { AND: [{ active: { equals: 1 } }, { name: { contains: 'Ada' } }] } },
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it('leaves the member unchanged when the related list is fully readable', async () => {
|
|
71
|
+
const config = {
|
|
72
|
+
db: { provider: 'sqlite', prismaClientConstructor: () => null },
|
|
73
|
+
lists: {
|
|
74
|
+
Tag: list({ fields: { name: text() }, access: { operation: { query: () => true } } }),
|
|
75
|
+
Post: list({ fields: { title: text(), tag: relationship({ ref: 'Tag' }) } }),
|
|
76
|
+
},
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
78
|
+
};
|
|
79
|
+
const where = { tag: { is: { name: { contains: 'news' } } } };
|
|
80
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
81
|
+
expect(resolved).toEqual(where);
|
|
82
|
+
});
|
|
83
|
+
it('resolves a fully denied related list to a never-matching member (no leak)', async () => {
|
|
84
|
+
const config = makeConfig();
|
|
85
|
+
const where = { widget: { is: { name: { contains: 'Gadget' } } } };
|
|
86
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
87
|
+
expect(resolved).toEqual({ id: { in: [] } });
|
|
88
|
+
});
|
|
89
|
+
it('preserves sibling conditions, ANDing the resolved id constraint for a denied member', async () => {
|
|
90
|
+
const config = makeConfig();
|
|
91
|
+
const where = {
|
|
92
|
+
AND: [{ title: { contains: 'hello' } }, { widget: { is: { name: { contains: 'Gadget' } } } }],
|
|
93
|
+
};
|
|
94
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
95
|
+
expect(resolved).toEqual({
|
|
96
|
+
AND: [{ title: { contains: 'hello' } }, { id: { in: [] } }],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('preserves sibling conditions alongside the access-scoped nested `is` clause', async () => {
|
|
100
|
+
const config = makeConfig();
|
|
101
|
+
const where = {
|
|
102
|
+
AND: [{ title: { contains: 'hello' } }, { author: { is: { name: { contains: 'Ada' } } } }],
|
|
103
|
+
};
|
|
104
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
105
|
+
expect(resolved).toEqual({
|
|
106
|
+
AND: [
|
|
107
|
+
{ title: { contains: 'hello' } },
|
|
108
|
+
{ author: { is: { AND: [{ active: { equals: 1 } }, { name: { contains: 'Ada' } }] } } },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it('composes with a to-many count marker member left untouched by this resolver', async () => {
|
|
113
|
+
const config = makeConfig();
|
|
114
|
+
const where = {
|
|
115
|
+
AND: [{ author: { is: { name: { contains: 'Ada' } } } }, { views: { gt: 10 } }],
|
|
116
|
+
};
|
|
117
|
+
const resolved = await resolveRelationshipLabelFilters(where, config.lists.Post, { session: null, context: makeContext() }, config);
|
|
118
|
+
expect(resolved).toEqual({
|
|
119
|
+
AND: [
|
|
120
|
+
{ author: { is: { AND: [{ active: { equals: 1 } }, { name: { contains: 'Ada' } }] } } },
|
|
121
|
+
{ views: { gt: 10 } },
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=relationship-label-filter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-label-filter.test.js","sourceRoot":"","sources":["../../src/access/relationship-label-filter.test.ts"],"names":[],"mappings":"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"}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -62,5 +62,5 @@ export declare function config(userConfig: OpenSaasConfig): OpenSaasConfig | Pro
|
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
export declare function list<TTypeInfo extends import('./types.js').TypeInfo>(config: ListConfigInput<TTypeInfo>): ListConfig<TTypeInfo>;
|
|
65
|
-
export type { OpenSaasConfig, OutputConfig, ListConfig, ListConfigInput, ListAccessControl, FieldConfig, BaseFieldConfig, TextField, IntegerField, CheckboxField, TimestampField, PasswordField, SelectField, RelationshipField, PrismaRelationResult, MultiColumnPrismaResult, JsonField, VirtualField, TypeDescriptor, TypeInfo, OperationAccess, Hooks, FieldHooks, FieldsWithTypeInfo, DatabaseConfig, SessionConfig, UIConfig, ListUIConfig, ListViewUIConfig, ThemeConfig, ThemePreset, ThemeColors, ThemeFonts, ThemeShadows, McpConfig, McpToolsConfig, McpAuthConfig, ListMcpConfig, McpCustomTool, FileMetadata, ImageMetadata, ImageTransformationResult, Plugin, PluginContext, GeneratedFiles, ResolveInputHookArgs, ValidateHookArgs, BeforeOperationHookArgs, AfterOperationHookArgs, FieldResolveInputHookArgs, FieldValidateHookArgs, FieldBeforeOperationHookArgs, FieldAfterOperationHookArgs, FieldResolveOutputHookArgs, } from './types.js';
|
|
65
|
+
export type { OpenSaasConfig, OutputConfig, ListConfig, ListConfigInput, ListAccessControl, FieldConfig, BaseFieldConfig, TextField, IntegerField, CheckboxField, TimestampField, PasswordField, SelectField, RelationshipField, PrismaRelationResult, MultiColumnPrismaResult, JsonField, VirtualField, TypeDescriptor, TypeInfo, OperationAccess, Hooks, FieldHooks, FieldsWithTypeInfo, DatabaseConfig, SessionConfig, UIConfig, ListUIConfig, ListViewUIConfig, BulkAction, BulkActionContext, BulkActionResult, BulkActionVariant, ItemViewUIConfig, RelationshipItemViewConfig, ThemeConfig, ThemePreset, ThemeColors, ThemeFonts, ThemeShadows, McpConfig, McpToolsConfig, McpAuthConfig, ListMcpConfig, McpCustomTool, FileMetadata, ImageMetadata, ImageTransformationResult, Plugin, PluginContext, GeneratedFiles, ResolveInputHookArgs, ValidateHookArgs, BeforeOperationHookArgs, AfterOperationHookArgs, FieldResolveInputHookArgs, FieldValidateHookArgs, FieldBeforeOperationHookArgs, FieldAfterOperationHookArgs, FieldResolveOutputHookArgs, } from './types.js';
|
|
66
66
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,eAAe,EAGhB,MAAM,YAAY,CAAA;AA8BnB;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,UAAU,EAAE,cAAc,GAAG,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAQ3F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,IAAI,CAAC,SAAS,SAAS,OAAO,YAAY,EAAE,QAAQ,EAClE,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,UAAU,CAAC,SAAS,CAAC,CAUvB;AAGD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,SAAS,EACT,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,KAAK,EACL,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,SAAS,EACT,cAAc,EACd,aAAa,EACb,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,yBAAyB,EAEzB,MAAM,EACN,aAAa,EACb,cAAc,EAEd,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EAEtB,yBAAyB,EACzB,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,eAAe,EAGhB,MAAM,YAAY,CAAA;AA8BnB;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,UAAU,EAAE,cAAc,GAAG,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAQ3F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,IAAI,CAAC,SAAS,SAAS,OAAO,YAAY,EAAE,QAAQ,EAClE,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,UAAU,CAAC,SAAS,CAAC,CAUvB;AAGD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,SAAS,EACT,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,KAAK,EACL,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,0BAA0B,EAC1B,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,SAAS,EACT,cAAc,EACd,aAAa,EACb,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,yBAAyB,EAEzB,MAAM,EACN,aAAa,EACb,cAAc,EAEd,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EAEtB,yBAAyB,EACzB,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AccessContext } from '../access/types.js';
|
|
2
|
+
import type { ListConfig, OpenSaasConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a list's query access is **statically denied** — provably no session
|
|
5
|
+
* can read any row without evaluating a session-dependent function.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the list view's `canDeleteList` static check (issue #733): query
|
|
8
|
+
* access is statically denied when it is absent (deny-by-default, matching the
|
|
9
|
+
* access engine) or the literal boolean `false`. A function — or `true` —
|
|
10
|
+
* cannot be decided up front, so the count is fetched through the secured
|
|
11
|
+
* context, which re-applies the real rule per row.
|
|
12
|
+
*
|
|
13
|
+
* A statically-denied list must render no nav count at all: a `0` there would
|
|
14
|
+
* imply "this list is empty" when the truth is "you may see none of it".
|
|
15
|
+
*/
|
|
16
|
+
export declare function isListQueryStaticallyDenied(listConfig: ListConfig<any>): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the access-scoped nav counts for the lists that opt in via
|
|
19
|
+
* `ui.navCount` (issue #735).
|
|
20
|
+
*
|
|
21
|
+
* The returned map is keyed by list key and contains an entry **only** for a
|
|
22
|
+
* list that:
|
|
23
|
+
*
|
|
24
|
+
* 1. opts in (`ui.navCount === true`) — no count query runs for any other list,
|
|
25
|
+
* 2. is not a singleton (a single-record list has no meaningful count), and
|
|
26
|
+
* 3. does not have statically-denied query access (see
|
|
27
|
+
* {@link isListQueryStaticallyDenied}).
|
|
28
|
+
*
|
|
29
|
+
* Each count is read through the secured `context.db`, whose `count` applies the
|
|
30
|
+
* access filter, so the number reflects exactly what the current session may
|
|
31
|
+
* see (a denied row is not counted). Counts are fetched concurrently.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveNavCounts(context: AccessContext, config: OpenSaasConfig): Promise<Record<string, number>>;
|
|
34
|
+
//# sourceMappingURL=nav-count.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nav-count.d.ts","sourceRoot":"","sources":["../../src/config/nav-count.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEvD,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAY,MAAM,YAAY,CAAA;AAWtE;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CAEzC,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,GAC1B,OAAO,CAKT;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA6BjC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getDbKey } from '../lib/case-utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a list's query access is **statically denied** — provably no session
|
|
4
|
+
* can read any row without evaluating a session-dependent function.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the list view's `canDeleteList` static check (issue #733): query
|
|
7
|
+
* access is statically denied when it is absent (deny-by-default, matching the
|
|
8
|
+
* access engine) or the literal boolean `false`. A function — or `true` —
|
|
9
|
+
* cannot be decided up front, so the count is fetched through the secured
|
|
10
|
+
* context, which re-applies the real rule per row.
|
|
11
|
+
*
|
|
12
|
+
* A statically-denied list must render no nav count at all: a `0` there would
|
|
13
|
+
* imply "this list is empty" when the truth is "you may see none of it".
|
|
14
|
+
*/
|
|
15
|
+
export function isListQueryStaticallyDenied(
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
17
|
+
listConfig) {
|
|
18
|
+
const queryAccess = listConfig.access?.operation?.query;
|
|
19
|
+
if (queryAccess === undefined)
|
|
20
|
+
return true;
|
|
21
|
+
if (typeof queryAccess === 'boolean')
|
|
22
|
+
return !queryAccess;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the access-scoped nav counts for the lists that opt in via
|
|
27
|
+
* `ui.navCount` (issue #735).
|
|
28
|
+
*
|
|
29
|
+
* The returned map is keyed by list key and contains an entry **only** for a
|
|
30
|
+
* list that:
|
|
31
|
+
*
|
|
32
|
+
* 1. opts in (`ui.navCount === true`) — no count query runs for any other list,
|
|
33
|
+
* 2. is not a singleton (a single-record list has no meaningful count), and
|
|
34
|
+
* 3. does not have statically-denied query access (see
|
|
35
|
+
* {@link isListQueryStaticallyDenied}).
|
|
36
|
+
*
|
|
37
|
+
* Each count is read through the secured `context.db`, whose `count` applies the
|
|
38
|
+
* access filter, so the number reflects exactly what the current session may
|
|
39
|
+
* see (a denied row is not counted). Counts are fetched concurrently.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveNavCounts(context, config) {
|
|
42
|
+
const counts = {};
|
|
43
|
+
const lists = config.lists ?? {};
|
|
44
|
+
const optedIn = Object.keys(lists).filter((listKey) => {
|
|
45
|
+
const listConfig = lists[listKey];
|
|
46
|
+
if (!listConfig)
|
|
47
|
+
return false;
|
|
48
|
+
if (listConfig.ui?.navCount !== true)
|
|
49
|
+
return false;
|
|
50
|
+
if (listConfig.isSingleton)
|
|
51
|
+
return false;
|
|
52
|
+
return !isListQueryStaticallyDenied(listConfig);
|
|
53
|
+
});
|
|
54
|
+
await Promise.all(optedIn.map(async (listKey) => {
|
|
55
|
+
const delegate = context.db?.[getDbKey(listKey)];
|
|
56
|
+
if (!delegate?.count)
|
|
57
|
+
return;
|
|
58
|
+
// A single list's count failing (a DB hiccup, a throwing access filter or
|
|
59
|
+
// hook) must not blank the whole admin chrome — `AdminUI` awaits this
|
|
60
|
+
// before rendering the shell. Degrade like the sibling `ListView`: log and
|
|
61
|
+
// omit just that badge. Other lists' counts are unaffected.
|
|
62
|
+
try {
|
|
63
|
+
counts[listKey] = await delegate.count();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Failed to resolve nav count for ${listKey}:`, error);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
return counts;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=nav-count.js.map
|