@opensaas/stack-core 0.29.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 +443 -0
- package/CLAUDE.md +4 -4
- 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 +206 -1
- 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/dist/mcp/handler.d.ts +11 -3
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +96 -20
- package/dist/mcp/handler.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 +286 -4
- 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/src/mcp/handler.ts +118 -28
- 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
|
@@ -91,12 +91,12 @@ export async function filterWritableFields(data, fieldConfigs, operation, args)
|
|
|
91
91
|
// Map each raw per-part column name contributed by a multi-column field
|
|
92
92
|
// (e.g. storage image()/file() in Keystone-parity mode) back to its OWNING
|
|
93
93
|
// declared field. These columns are injected into the write payload by the
|
|
94
|
-
// field's `splitColumns
|
|
95
|
-
// as their own entries in `fieldConfigs`, so without this map they
|
|
96
|
-
// the #564 undeclared-key reject below.
|
|
94
|
+
// field's `splitColumns`, AFTER validation (#789), and are intentionally NOT
|
|
95
|
+
// declared as their own entries in `fieldConfigs`, so without this map they
|
|
96
|
+
// would trip the #564 undeclared-key reject below.
|
|
97
97
|
//
|
|
98
98
|
// SECURITY (#568): a raw column must NOT be blanket-passed through. The hooks
|
|
99
|
-
// layer (`
|
|
99
|
+
// layer (`splitMultiColumnFields`) only gates the owning field when the
|
|
100
100
|
// LOGICAL key (e.g. `media`) is present, because it iterates declared fields,
|
|
101
101
|
// not data keys. A non-sudo caller who supplies the raw columns DIRECTLY
|
|
102
102
|
// (`data: { media_url, media_size }`) never produces that logical key, so that
|
|
@@ -141,8 +141,8 @@ export async function filterWritableFields(data, fieldConfigs, operation, args)
|
|
|
141
141
|
// they must NOT be blanket-passed through either: gate each one by its
|
|
142
142
|
// OWNING field's write access (see the SECURITY note where the map is built).
|
|
143
143
|
// This is the real gate for callers who supply the raw columns directly,
|
|
144
|
-
// because the logical-key gate in `
|
|
145
|
-
//
|
|
144
|
+
// because the logical-key gate in `splitMultiColumnFields` never fires for
|
|
145
|
+
// them. Denied (non-sudo) throws — same fail-loud behaviour as a denied
|
|
146
146
|
// declared field (#568); allowed (or sudo, via `checkFieldAccess`) passes
|
|
147
147
|
// through, preserving the legitimate multi-column write path.
|
|
148
148
|
const splitColumnOwner = splitColumnOwners.get(fieldName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-access.js","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,8EAA8E;AAC9E,2BAA2B;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,WAAoC,EACpC,SAAuC,EACvC,IAKC;IAED,iCAAiC;IACjC,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAA,CAAC,8BAA8B;IAC5C,CAAC;IAED,MAAM,aAAa,GAAG,WAAW,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,IAAI,CAAA,CAAC,yCAAyC;IACvD,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;QACjC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS;KAC6B,CAAC,CAAA;IAEzC,kCAAkC;IAClC,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,kCAAkC;IAClC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,mDAAmD;IACnD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,IAA6B,EAAE,MAA+B;IACnF,KAAK,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACtD,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACxD,kDAAkD;YAClD,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;gBAC1B,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;oBACnC,OAAO,KAAK,CAAA;gBACd,CAAC;YACH,CAAC;iBAAM,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;gBAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,GAAG,EAAE,CAAC;oBAChC,OAAO,KAAK,CAAA;gBACd,CAAC;YACH,CAAC;YACD,qCAAqC;QACvC,CAAC;aAAM,CAAC;YACN,wBAAwB;YACxB,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,KAAK,CAAA;YACd,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,IAAO,EACP,YAOC,EACD,SAA8B,EAC9B,IAKC;IAED,MAAM,QAAQ,GAA4B,EAAE,CAAA;IAE5C,oDAAoD;IACpD,6EAA6E;IAC7E,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAA;IAC1C,wEAAwE;IACxE,2EAA2E;IAC3E,2EAA2E;IAC3E
|
|
1
|
+
{"version":3,"file":"field-access.js","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { filterReadableFields } from './field-visibility.js';
|
|
3
|
-
import { executeFieldResolveInputHooks } from '../hooks/index.js';
|
|
3
|
+
import { executeFieldResolveInputHooks, splitMultiColumnFields } from '../hooks/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Generic core wiring for multi-column fields (the contract storage
|
|
6
6
|
* image()/file() use in Keystone-parity mode — see ADR-0006). These tests are
|
|
7
7
|
* field-agnostic: they assert that ANY field implementing
|
|
8
8
|
* getColumnNames/assembleColumns/splitColumns is assembled on read (raw columns
|
|
9
9
|
* stripped) and split on write.
|
|
10
|
+
*
|
|
11
|
+
* The write side is split across two phases (#789): `executeFieldResolveInputHooks`
|
|
12
|
+
* (Phase 1.5) resolves the field's value under its LOGICAL key only — no split —
|
|
13
|
+
* so that validation (Phase 2-3, not exercised by these field-agnostic unit
|
|
14
|
+
* tests; see the Hook Pipeline / Write Pipeline test suites) runs against the
|
|
15
|
+
* logical value first. `splitMultiColumnFields` (Phase 4) then replaces the
|
|
16
|
+
* logical key with its physical per-part columns, AFTER validation has passed.
|
|
10
17
|
*/
|
|
11
18
|
// A minimal multi-column field: two physical columns `m_url` and `m_size`
|
|
12
19
|
// assembled into `{ url, size }` and split back. Optionally carries field-level
|
|
@@ -77,24 +84,51 @@ describe('multi-column read assembly (filterReadableFields)', () => {
|
|
|
77
84
|
expect('media' in result).toBe(false);
|
|
78
85
|
});
|
|
79
86
|
});
|
|
80
|
-
describe('multi-column
|
|
87
|
+
describe('multi-column resolveInput (executeFieldResolveInputHooks) does NOT split (#789)', () => {
|
|
81
88
|
const fields = { media: multiColumnField() };
|
|
82
|
-
it('
|
|
89
|
+
it('resolves the field under its LOGICAL key and leaves it unsplit', async () => {
|
|
83
90
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
84
91
|
const result = await executeFieldResolveInputHooks(inputData, { ...inputData }, fields, 'create', makeContext(), 'Post');
|
|
92
|
+
// Still under the logical key — no split, no per-part columns yet.
|
|
93
|
+
expect(result).toEqual({ media: { url: 'https://x/y.jpg', size: 99 } });
|
|
94
|
+
expect('m_url' in result).toBe(false);
|
|
95
|
+
expect('m_size' in result).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it('passes an unrecognised value through unsplit, under the logical key', async () => {
|
|
98
|
+
// The field's own resolveInput fallback (see fixture: "unknown → return as
|
|
99
|
+
// is and let validation catch it") must be able to hand this value to
|
|
100
|
+
// validation BEFORE any split happens.
|
|
101
|
+
const inputData = { media: 'not-a-valid-shape' };
|
|
102
|
+
const result = await executeFieldResolveInputHooks(inputData, { ...inputData }, fields, 'update', makeContext(), 'Post');
|
|
103
|
+
expect(result).toEqual({ media: 'not-a-valid-shape' });
|
|
104
|
+
});
|
|
105
|
+
it('does not touch the field when it is absent from the write', async () => {
|
|
106
|
+
const inputData = { title: 'no media in payload' };
|
|
107
|
+
const result = await executeFieldResolveInputHooks(inputData, { ...inputData },
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- inline field configs
|
|
109
|
+
{ ...fields, title: { type: 'text' } }, 'update', makeContext(), 'Post');
|
|
110
|
+
expect(result).toEqual({ title: 'no media in payload' });
|
|
111
|
+
expect('media' in result).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('multi-column write split (splitMultiColumnFields, AFTER validation — #789)', () => {
|
|
115
|
+
const fields = { media: multiColumnField() };
|
|
116
|
+
it('splits the logical value into per-part columns and removes the logical key', async () => {
|
|
117
|
+
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
118
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'create', makeContext());
|
|
85
119
|
expect(result).toEqual({ m_url: 'https://x/y.jpg', m_size: 99 });
|
|
86
120
|
expect('media' in result).toBe(false);
|
|
87
121
|
});
|
|
88
122
|
it('splitting null clears all per-part columns', async () => {
|
|
89
123
|
const inputData = { media: null };
|
|
90
|
-
const result = await
|
|
124
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'update', makeContext());
|
|
91
125
|
expect(result).toEqual({ m_url: null, m_size: null });
|
|
92
126
|
});
|
|
93
127
|
it('does not touch the columns when the logical field is absent from the write', async () => {
|
|
94
128
|
const inputData = { title: 'no media in payload' };
|
|
95
|
-
const result = await
|
|
129
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData },
|
|
96
130
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- inline field configs
|
|
97
|
-
{ ...fields, title: { type: 'text' } }, 'update', makeContext()
|
|
131
|
+
{ ...fields, title: { type: 'text' } }, 'update', makeContext());
|
|
98
132
|
expect(result).toEqual({ title: 'no media in payload' });
|
|
99
133
|
expect('m_url' in result).toBe(false);
|
|
100
134
|
});
|
|
@@ -103,7 +137,7 @@ describe('multi-column write split respects field-level write access', () => {
|
|
|
103
137
|
it('does NOT write any per-part columns when update access is denied', async () => {
|
|
104
138
|
const fields = { media: multiColumnField({ update: () => false }) };
|
|
105
139
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
106
|
-
const result = await
|
|
140
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'update', makeContext());
|
|
107
141
|
// The logical key is dropped (it is not a real column) AND none of its
|
|
108
142
|
// per-part columns are written — identical to how filterWritableFields
|
|
109
143
|
// drops a denied single-column field.
|
|
@@ -115,14 +149,14 @@ describe('multi-column write split respects field-level write access', () => {
|
|
|
115
149
|
it('does NOT write any per-part columns when create access is denied', async () => {
|
|
116
150
|
const fields = { media: multiColumnField({ create: () => false }) };
|
|
117
151
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
118
|
-
const result = await
|
|
152
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'create', makeContext());
|
|
119
153
|
expect(result).toEqual({});
|
|
120
154
|
expect('m_url' in result).toBe(false);
|
|
121
155
|
});
|
|
122
156
|
it('still splits/writes the columns when write access is granted', async () => {
|
|
123
157
|
const fields = { media: multiColumnField({ update: () => true, create: () => true }) };
|
|
124
158
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
125
|
-
const result = await
|
|
159
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'update', makeContext());
|
|
126
160
|
expect(result).toEqual({ m_url: 'https://x/y.jpg', m_size: 99 });
|
|
127
161
|
expect('media' in result).toBe(false);
|
|
128
162
|
});
|
|
@@ -130,19 +164,19 @@ describe('multi-column write split respects field-level write access', () => {
|
|
|
130
164
|
// A field that denies `update` must still be writable on `create`.
|
|
131
165
|
const fields = { media: multiColumnField({ update: () => false }) };
|
|
132
166
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
133
|
-
const result = await
|
|
167
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'create', makeContext());
|
|
134
168
|
expect(result).toEqual({ m_url: 'https://x/y.jpg', m_size: 99 });
|
|
135
169
|
});
|
|
136
170
|
it('sudo bypasses the field-access gate and still splits', async () => {
|
|
137
171
|
const fields = { media: multiColumnField({ update: () => false }) };
|
|
138
172
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
139
|
-
const result = await
|
|
173
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'update', makeContext({ isSudo: true }));
|
|
140
174
|
expect(result).toEqual({ m_url: 'https://x/y.jpg', m_size: 99 });
|
|
141
175
|
});
|
|
142
176
|
it('a multi-column field WITHOUT field-level access splits exactly as before', async () => {
|
|
143
177
|
const fields = { media: multiColumnField() };
|
|
144
178
|
const inputData = { media: { url: 'https://x/y.jpg', size: 99 } };
|
|
145
|
-
const result = await
|
|
179
|
+
const result = await splitMultiColumnFields(inputData, { ...inputData }, fields, 'update', makeContext());
|
|
146
180
|
expect(result).toEqual({ m_url: 'https://x/y.jpg', m_size: 99 });
|
|
147
181
|
});
|
|
148
182
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-column-read-write.test.js","sourceRoot":"","sources":["../../src/access/multi-column-read-write.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"multi-column-read-write.test.js","sourceRoot":"","sources":["../../src/access/multi-column-read-write.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Session, AccessContext } from './types.js';
|
|
2
|
+
import type { OpenSaasConfig, ListConfig, FieldConfig } from '../config/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Access-scoped to-many relationship counts for the admin list view (issue
|
|
5
|
+
* #732).
|
|
6
|
+
*
|
|
7
|
+
* A to-many relationship column shows the count of the related rows the session
|
|
8
|
+
* may see — fetched in the SAME query as the row via Prisma's filtered
|
|
9
|
+
* `_count` (`_count: { select: { orders: { where: <access filter> } } }`), so no
|
|
10
|
+
* per-row query is issued and the count can never include rows the session
|
|
11
|
+
* cannot read. This module is the single place the related list's
|
|
12
|
+
* operation-level `query` access is folded into that `_count`, mirroring how
|
|
13
|
+
* `buildIncludeWithAccessControl` folds it into relation includes.
|
|
14
|
+
*
|
|
15
|
+
* It also resolves the count Filter spec's markers: Prisma cannot compare a
|
|
16
|
+
* relation count in a `where`, so a to-many relationship's Filter spec emits a
|
|
17
|
+
* {@link RELATIONSHIP_COUNT_FILTER_KEY} marker that
|
|
18
|
+
* {@link resolveRelationshipCountFilters} turns into an access-scoped
|
|
19
|
+
* `{ id: { in } }` before the query runs — never leaking counts of related rows
|
|
20
|
+
* the session cannot see.
|
|
21
|
+
*/
|
|
22
|
+
type CountArgs = {
|
|
23
|
+
session: Session | null;
|
|
24
|
+
context: AccessContext;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Whether a field is a to-many relationship — the only field kind that carries a
|
|
28
|
+
* relationship count (a to-one relationship has at most one related row).
|
|
29
|
+
*/
|
|
30
|
+
export declare function isToManyRelationshipField(field: FieldConfig | undefined): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Build the object to place at `include._count.select` for a list's to-many
|
|
33
|
+
* relationships, with each related list's `query` access folded into the
|
|
34
|
+
* per-relation `where` so the returned counts are access-scoped. A relationship
|
|
35
|
+
* whose related list is fully denied is omitted (its count renders as 0). Returns
|
|
36
|
+
* `undefined` when the list has no countable to-many relationships.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildRelationshipCountSelect(listConfig: ListConfig<any>, args: CountArgs, config: OpenSaasConfig): Promise<Record<string, unknown> | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Merge a filter-member's non-resolved sibling conditions with a resolved
|
|
41
|
+
* access-scoped fragment (e.g. a count marker's `{ id: { in } }`, or a to-one
|
|
42
|
+
* label filter's access-scoped `is`). With no siblings (the guaranteed case
|
|
43
|
+
* today) this is just the resolved fragment. When a sibling shares a key with
|
|
44
|
+
* the resolved fragment — a contrived case that cannot arise under the current
|
|
45
|
+
* one-condition-per-member invariant — both are ANDed so neither condition is
|
|
46
|
+
* silently lost. Shared by both relationship resolvers in this module and in
|
|
47
|
+
* `relationship-label-filter.ts`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function mergeResolvedMember(siblings: Record<string, unknown>, resolved: Record<string, unknown>): Record<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* Replace any to-many relationship count-filter markers in a filter `where` with
|
|
52
|
+
* access-scoped `{ id: { in } }` fragments. Markers only ever appear as
|
|
53
|
+
* top-level AND members (the pure filter engine pushes each field condition into
|
|
54
|
+
* the top-level AND, and never nests a marker inside the free-text OR), so this
|
|
55
|
+
* walks only the top level. Returns the `where` unchanged when it contains no
|
|
56
|
+
* markers, so lists without count filters pay nothing.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveRelationshipCountFilters(where: Record<string, unknown> | undefined, listConfig: ListConfig<any>, listKey: string, args: CountArgs, config: OpenSaasConfig): Promise<Record<string, unknown> | undefined>;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=relationship-count.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-count.d.ts","sourceRoot":"","sources":["../../src/access/relationship-count.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAgB,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAMjF;;;;;;;;;;;;;;;;;;GAkBG;AAEH,KAAK,SAAS,GAAG;IACf,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,CAAA;CACvB,CAAA;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CASjF;AAkCD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAEhD,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,EAC3B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAQ9C;AA0HD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB;AAED;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAE1C,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,EAC3B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAuD9C"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { RELATIONSHIP_COUNT_FILTER_KEY } from '../filter/types.js';
|
|
2
|
+
import { checkAccess, getRelatedListConfig } from './engine.js';
|
|
3
|
+
import { getDbKey } from '../lib/case-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Whether a field is a to-many relationship — the only field kind that carries a
|
|
6
|
+
* relationship count (a to-one relationship has at most one related row).
|
|
7
|
+
*/
|
|
8
|
+
export function isToManyRelationshipField(field) {
|
|
9
|
+
return (field?.type === 'relationship' &&
|
|
10
|
+
'many' in field &&
|
|
11
|
+
field.many === true &&
|
|
12
|
+
'ref' in field &&
|
|
13
|
+
typeof field.ref === 'string' &&
|
|
14
|
+
field.ref.length > 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the related list's operation-level `query` access for one to-many
|
|
18
|
+
* relationship into the entry its `_count` select needs. Returns `null` when the
|
|
19
|
+
* field is not a resolvable to-many relationship.
|
|
20
|
+
*/
|
|
21
|
+
async function relationshipCountAccessEntry(field, args, config) {
|
|
22
|
+
if (!field || !isToManyRelationshipField(field) || !('ref' in field)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const ref = field.ref;
|
|
26
|
+
if (typeof ref !== 'string')
|
|
27
|
+
return null;
|
|
28
|
+
const related = getRelatedListConfig(ref, config);
|
|
29
|
+
if (!related)
|
|
30
|
+
return null;
|
|
31
|
+
const queryAccess = related.listConfig.access?.operation?.query;
|
|
32
|
+
const result = await checkAccess(queryAccess, { session: args.session, context: args.context });
|
|
33
|
+
if (result === false)
|
|
34
|
+
return { kind: 'denied' };
|
|
35
|
+
if (typeof result === 'object')
|
|
36
|
+
return { kind: 'scoped', where: result };
|
|
37
|
+
return { kind: 'all' };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the object to place at `include._count.select` for a list's to-many
|
|
41
|
+
* relationships, with each related list's `query` access folded into the
|
|
42
|
+
* per-relation `where` so the returned counts are access-scoped. A relationship
|
|
43
|
+
* whose related list is fully denied is omitted (its count renders as 0). Returns
|
|
44
|
+
* `undefined` when the list has no countable to-many relationships.
|
|
45
|
+
*/
|
|
46
|
+
export async function buildRelationshipCountSelect(
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
48
|
+
listConfig, args, config) {
|
|
49
|
+
const select = {};
|
|
50
|
+
for (const [fieldName, field] of Object.entries(listConfig.fields)) {
|
|
51
|
+
const entry = await relationshipCountAccessEntry(field, args, config);
|
|
52
|
+
if (entry === null || entry.kind === 'denied')
|
|
53
|
+
continue;
|
|
54
|
+
select[fieldName] = entry.kind === 'scoped' ? { where: entry.where } : true;
|
|
55
|
+
}
|
|
56
|
+
return Object.keys(select).length > 0 ? select : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Read a to-many relationship's count off a fetched row's `_count` payload. */
|
|
59
|
+
function readRelationshipCount(row, fieldName) {
|
|
60
|
+
const counts = row._count;
|
|
61
|
+
if (counts && typeof counts === 'object') {
|
|
62
|
+
const value = counts[fieldName];
|
|
63
|
+
if (typeof value === 'number')
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
/** Whether a count satisfies a Filter operator/value comparison. */
|
|
69
|
+
function matchesCount(count, operator, value) {
|
|
70
|
+
switch (operator) {
|
|
71
|
+
case 'eq':
|
|
72
|
+
return count === value;
|
|
73
|
+
case 'gt':
|
|
74
|
+
return count > value;
|
|
75
|
+
case 'gte':
|
|
76
|
+
return count >= value;
|
|
77
|
+
case 'lt':
|
|
78
|
+
return count < value;
|
|
79
|
+
case 'lte':
|
|
80
|
+
return count <= value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function asCountDelegate(value) {
|
|
84
|
+
if (value && typeof value === 'object' && 'findMany' in value) {
|
|
85
|
+
const candidate = value;
|
|
86
|
+
if (typeof candidate.findMany === 'function') {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/** Extract the `RelationshipCountFilterMarker` from a condition value, if present. */
|
|
93
|
+
function readCountMarker(value) {
|
|
94
|
+
if (!value || typeof value !== 'object')
|
|
95
|
+
return null;
|
|
96
|
+
const marker = value[RELATIONSHIP_COUNT_FILTER_KEY];
|
|
97
|
+
if (!marker || typeof marker !== 'object')
|
|
98
|
+
return null;
|
|
99
|
+
const { operator, value: n } = marker;
|
|
100
|
+
if ((operator === 'eq' ||
|
|
101
|
+
operator === 'gt' ||
|
|
102
|
+
operator === 'gte' ||
|
|
103
|
+
operator === 'lt' ||
|
|
104
|
+
operator === 'lte') &&
|
|
105
|
+
typeof n === 'number') {
|
|
106
|
+
return { operator, value: n };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolve one to-many relationship count-filter marker into a Prisma `where`
|
|
112
|
+
* fragment constraining the parent by id. Runs a single access-scoped read
|
|
113
|
+
* through the SECURED context — never a raw/unscoped query and never a per-row
|
|
114
|
+
* query — computing the access-visible count per parent and keeping the ids
|
|
115
|
+
* whose count satisfies the comparison. A fully-denied related list makes every
|
|
116
|
+
* count 0, resolved without any query.
|
|
117
|
+
*/
|
|
118
|
+
async function resolveOneCountFilter(
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
120
|
+
listConfig, listKey, fieldName, marker, args, config) {
|
|
121
|
+
const field = listConfig.fields[fieldName];
|
|
122
|
+
const entry = await relationshipCountAccessEntry(field, args, config);
|
|
123
|
+
// Not resolvable (shouldn't happen for a marker the spec emitted) → no-op.
|
|
124
|
+
if (entry === null)
|
|
125
|
+
return {};
|
|
126
|
+
// Related list fully denied → every parent's access-visible count is 0.
|
|
127
|
+
if (entry.kind === 'denied') {
|
|
128
|
+
return matchesCount(0, marker.operator, marker.value) ? {} : { id: { in: [] } };
|
|
129
|
+
}
|
|
130
|
+
const delegate = asCountDelegate(args.context.db[getDbKey(listKey)]);
|
|
131
|
+
if (!delegate)
|
|
132
|
+
return {};
|
|
133
|
+
const countSelect = entry.kind === 'scoped' ? { where: entry.where } : true;
|
|
134
|
+
// This over-fetches the parent's scalar columns — it reads only `id` + `_count`
|
|
135
|
+
// per row yet materialises every scalar. It is left un-narrowed on purpose: the
|
|
136
|
+
// secured `context.db` `findMany` does NOT honour Prisma `select` (it
|
|
137
|
+
// warns-and-ignores it and returns the full access-filtered record — see
|
|
138
|
+
// `warnIfSelectIgnored` in context/index.ts and the "Narrowing Reads" note in
|
|
139
|
+
// packages/core/CLAUDE.md). The only supported narrowing is `include`/fragment
|
|
140
|
+
// `query`, neither of which can drop scalar columns. So adding
|
|
141
|
+
// `select: { id: true, _count: {...} }` here would be a silent no-op that also
|
|
142
|
+
// trips the ignore-warning, not a real projection. The count stays access-scoped
|
|
143
|
+
// via the filtered `_count` include below regardless; trimming the projection
|
|
144
|
+
// would first require the read pipeline to honour `select`.
|
|
145
|
+
const rows = await delegate.findMany({
|
|
146
|
+
include: { _count: { select: { [fieldName]: countSelect } } },
|
|
147
|
+
});
|
|
148
|
+
const matchingIds = [];
|
|
149
|
+
if (Array.isArray(rows)) {
|
|
150
|
+
for (const row of rows) {
|
|
151
|
+
if (!row || typeof row !== 'object')
|
|
152
|
+
continue;
|
|
153
|
+
const record = row;
|
|
154
|
+
if (matchesCount(readRelationshipCount(record, fieldName), marker.operator, marker.value)) {
|
|
155
|
+
matchingIds.push(String(record.id));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { id: { in: matchingIds } };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Merge a filter-member's non-resolved sibling conditions with a resolved
|
|
163
|
+
* access-scoped fragment (e.g. a count marker's `{ id: { in } }`, or a to-one
|
|
164
|
+
* label filter's access-scoped `is`). With no siblings (the guaranteed case
|
|
165
|
+
* today) this is just the resolved fragment. When a sibling shares a key with
|
|
166
|
+
* the resolved fragment — a contrived case that cannot arise under the current
|
|
167
|
+
* one-condition-per-member invariant — both are ANDed so neither condition is
|
|
168
|
+
* silently lost. Shared by both relationship resolvers in this module and in
|
|
169
|
+
* `relationship-label-filter.ts`.
|
|
170
|
+
*/
|
|
171
|
+
export function mergeResolvedMember(siblings, resolved) {
|
|
172
|
+
if (Object.keys(siblings).length === 0)
|
|
173
|
+
return resolved;
|
|
174
|
+
const collides = Object.keys(resolved).some((key) => key in siblings);
|
|
175
|
+
return collides ? { AND: [siblings, resolved] } : { ...siblings, ...resolved };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Replace any to-many relationship count-filter markers in a filter `where` with
|
|
179
|
+
* access-scoped `{ id: { in } }` fragments. Markers only ever appear as
|
|
180
|
+
* top-level AND members (the pure filter engine pushes each field condition into
|
|
181
|
+
* the top-level AND, and never nests a marker inside the free-text OR), so this
|
|
182
|
+
* walks only the top level. Returns the `where` unchanged when it contains no
|
|
183
|
+
* markers, so lists without count filters pay nothing.
|
|
184
|
+
*/
|
|
185
|
+
export async function resolveRelationshipCountFilters(where,
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
187
|
+
listConfig, listKey, args, config) {
|
|
188
|
+
if (!where)
|
|
189
|
+
return where;
|
|
190
|
+
const andValue = where.AND;
|
|
191
|
+
const members = Array.isArray(andValue)
|
|
192
|
+
? andValue
|
|
193
|
+
: [where];
|
|
194
|
+
const findMarker = (member) => {
|
|
195
|
+
for (const key of Object.keys(member)) {
|
|
196
|
+
if (!isToManyRelationshipField(listConfig.fields[key]))
|
|
197
|
+
continue;
|
|
198
|
+
const marker = readCountMarker(member[key]);
|
|
199
|
+
if (marker)
|
|
200
|
+
return { field: key, marker };
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
if (!members.some((member) => findMarker(member) !== null)) {
|
|
205
|
+
return where;
|
|
206
|
+
}
|
|
207
|
+
const resolvedMembers = [];
|
|
208
|
+
for (const member of members) {
|
|
209
|
+
const found = findMarker(member);
|
|
210
|
+
if (!found) {
|
|
211
|
+
resolvedMembers.push(member);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const resolved = await resolveOneCountFilter(listConfig, listKey, found.field, found.marker, args, config);
|
|
215
|
+
// Preserve any sibling conditions co-present on this member rather than
|
|
216
|
+
// replacing it wholesale with the resolved `{ id: { in } }`. The filter engine
|
|
217
|
+
// currently guarantees each AND-member (and the no-AND single object) carries
|
|
218
|
+
// exactly one field condition, so `siblings` is empty today and this equals the
|
|
219
|
+
// previous wholesale replacement — but if a future engine change ever merged
|
|
220
|
+
// multiple conditions into one member, spreading keeps the marker's siblings
|
|
221
|
+
// from being silently dropped.
|
|
222
|
+
const siblings = { ...member };
|
|
223
|
+
delete siblings[found.field];
|
|
224
|
+
resolvedMembers.push(mergeResolvedMember(siblings, resolved));
|
|
225
|
+
}
|
|
226
|
+
// Drop no-op members (a fully-denied related list where 0 satisfies the
|
|
227
|
+
// comparison resolves to `{}` — matches everything, so it need not be ANDed).
|
|
228
|
+
const effective = resolvedMembers.filter((member) => Object.keys(member).length > 0);
|
|
229
|
+
if (effective.length === 0)
|
|
230
|
+
return undefined;
|
|
231
|
+
return effective.length === 1 ? effective[0] : { AND: effective };
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=relationship-count.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-count.js","sourceRoot":"","sources":["../../src/access/relationship-count.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-count.test.d.ts","sourceRoot":"","sources":["../../src/access/relationship-count.test.ts"],"names":[],"mappings":""}
|