@byline/core 2.5.1 → 2.5.2
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.
|
@@ -120,23 +120,35 @@ export interface CombinatorFilter {
|
|
|
120
120
|
children: DocumentFilter[];
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
|
-
* A predicate over a document-version column (`status`, `path`).
|
|
124
|
-
* from `FieldFilter` — these columns live on `document_versions`
|
|
125
|
-
* not on the EAV stores, so they
|
|
126
|
-
* comparison rather than an `EXISTS`
|
|
123
|
+
* A predicate over a document-version column (`status`, `path`, `id`).
|
|
124
|
+
* Distinct from `FieldFilter` — these columns live on `document_versions`
|
|
125
|
+
* itself (or the current-documents view), not on the EAV stores, so they
|
|
126
|
+
* compile to a direct outer-scope column comparison rather than an `EXISTS`
|
|
127
|
+
* subquery.
|
|
127
128
|
*
|
|
128
|
-
* Produced by `parse-where`
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
129
|
+
* Produced by `parse-where` for two reasons:
|
|
130
|
+
*
|
|
131
|
+
* - `status` / `path` appearing *inside* a combinator (`$or` / `$and`
|
|
132
|
+
* child) or inside a relation sub-clause. At the top level the same
|
|
133
|
+
* keys are intercepted as `ParsedWhere.status` / `ParsedWhere.pathFilter`
|
|
134
|
+
* because they map to direct adapter parameters there; inside a
|
|
135
|
+
* combinator that mapping no longer makes sense (you can't OR-combine
|
|
136
|
+
* with the outer scalar parameter), so they downshift to this filter.
|
|
137
|
+
*
|
|
138
|
+
* - `id` at *any* scope. Unlike `status` (single equality, used in many
|
|
139
|
+
* non-filter call sites) and `path` (needs the `pathProjection` join
|
|
140
|
+
* against `byline_document_paths`), `id` is a plain column on the
|
|
141
|
+
* current-documents view comparable directly at every scope. Skipping
|
|
142
|
+
* a top-level scalar form keeps the surface area small.
|
|
143
|
+
*
|
|
144
|
+
* `value` is widened beyond `string | null` so the `$in` / `$nin` operators
|
|
145
|
+
* (the headline use case for `id`) can carry array operands.
|
|
134
146
|
*/
|
|
135
147
|
export interface DocumentColumnFilter {
|
|
136
148
|
kind: 'docColumn';
|
|
137
|
-
column: 'status' | 'path';
|
|
149
|
+
column: 'status' | 'path' | 'id';
|
|
138
150
|
operator: FieldFilterOperator;
|
|
139
|
-
value: string | null
|
|
151
|
+
value: string | number | boolean | null | Array<string | number>;
|
|
140
152
|
}
|
|
141
153
|
/**
|
|
142
154
|
* Any filter that can appear in a `findDocuments` call — a direct field
|
|
@@ -10,7 +10,7 @@ import { fieldTypeToStore } from '../storage/field-store-map.js';
|
|
|
10
10
|
// Document-level reserved keys
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
/** Where clause keys that map to document-level columns, not EAV stores. */
|
|
13
|
-
const DOCUMENT_LEVEL_KEYS = new Set(['status', 'path', 'query']);
|
|
13
|
+
const DOCUMENT_LEVEL_KEYS = new Set(['status', 'path', 'query', 'id']);
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Document-level sort columns
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -225,6 +225,26 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
|
|
|
225
225
|
}
|
|
226
226
|
continue;
|
|
227
227
|
}
|
|
228
|
+
// `id` always downshifts to a `DocumentColumnFilter` — no top-level
|
|
229
|
+
// scalar form. Unlike `status` (single equality, used in many non-filter
|
|
230
|
+
// call sites) and `path` (needs the `pathProjection` join), `id` is a
|
|
231
|
+
// plain column on the current-documents view comparable directly at any
|
|
232
|
+
// scope. Unifying all three scopes through the same downshift keeps the
|
|
233
|
+
// surface area small. Note: the value is passed through verbatim — we
|
|
234
|
+
// don't `String(...)` it the way status/path do, because the headline
|
|
235
|
+
// use case is `$in` / `$nin` carrying an array of document ids.
|
|
236
|
+
if (key === 'id') {
|
|
237
|
+
const parsed = normaliseToOperator(rawValue);
|
|
238
|
+
if (parsed) {
|
|
239
|
+
result.filters.push({
|
|
240
|
+
kind: 'docColumn',
|
|
241
|
+
column: 'id',
|
|
242
|
+
operator: parsed.operator,
|
|
243
|
+
value: parsed.value,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
228
248
|
// --- Field-level keys --------------------------------------------------
|
|
229
249
|
const field = definition.fields.find((f) => f.name === key);
|
|
230
250
|
if (!field)
|
|
@@ -625,6 +625,65 @@ describe('parseWhere — combinators', () => {
|
|
|
625
625
|
});
|
|
626
626
|
});
|
|
627
627
|
// ---------------------------------------------------------------------------
|
|
628
|
+
// parseWhere — `id` reserved key
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
//
|
|
631
|
+
// `id` is the logical document id (`document_id` on the current-documents
|
|
632
|
+
// view). It is reserved at every scope and always downshifts to a
|
|
633
|
+
// `DocumentColumnFilter` — no top-level scalar form like `status` /
|
|
634
|
+
// `pathFilter`, because `id` is a plain column comparable directly.
|
|
635
|
+
describe('parseWhere — `id` reserved key', () => {
|
|
636
|
+
it('emits a docColumn filter for a bare top-level `id` value', async () => {
|
|
637
|
+
const result = await parseWhere({ id: 'doc-123' }, testCollection);
|
|
638
|
+
expect(result.filters).toEqual([
|
|
639
|
+
{ kind: 'docColumn', column: 'id', operator: '$eq', value: 'doc-123' },
|
|
640
|
+
]);
|
|
641
|
+
});
|
|
642
|
+
it('emits a docColumn filter with array value for top-level `id: { $in }`', async () => {
|
|
643
|
+
// The headline use case — batch lookup by a list of document ids.
|
|
644
|
+
// Value passes through verbatim (no `String(...)` coercion) so the
|
|
645
|
+
// array survives to the adapter's `$in` SQL builder.
|
|
646
|
+
const ids = ['doc-1', 'doc-2', 'doc-3'];
|
|
647
|
+
const result = await parseWhere({ id: { $in: ids } }, testCollection);
|
|
648
|
+
expect(result.filters).toEqual([
|
|
649
|
+
{ kind: 'docColumn', column: 'id', operator: '$in', value: ids },
|
|
650
|
+
]);
|
|
651
|
+
});
|
|
652
|
+
it('emits a docColumn filter for `id` inside an $or combinator', async () => {
|
|
653
|
+
const result = await parseWhere({ $or: [{ id: 'doc-a' }, { id: 'doc-b' }] }, testCollection);
|
|
654
|
+
expect(result.filters).toHaveLength(1);
|
|
655
|
+
expect(result.filters[0]).toMatchObject({
|
|
656
|
+
kind: 'or',
|
|
657
|
+
children: [
|
|
658
|
+
{ kind: 'docColumn', column: 'id', operator: '$eq', value: 'doc-a' },
|
|
659
|
+
{ kind: 'docColumn', column: 'id', operator: '$eq', value: 'doc-b' },
|
|
660
|
+
],
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
it('emits a docColumn filter for `id` inside a nested relation sub-where', async () => {
|
|
664
|
+
// Reserved-key precedence — same as `status` / `path`: inside a
|
|
665
|
+
// relation hop, `id` refers to the target version's document_id, not
|
|
666
|
+
// a field of the same name.
|
|
667
|
+
const result = await parseWhere({ category: { id: 'cat-news' } }, testCollection, ctx);
|
|
668
|
+
expect(result.filters).toHaveLength(1);
|
|
669
|
+
expect(result.filters[0]).toEqual({
|
|
670
|
+
kind: 'relation',
|
|
671
|
+
fieldName: 'category',
|
|
672
|
+
targetCollectionId: 'id-test-categories',
|
|
673
|
+
nested: [{ kind: 'docColumn', column: 'id', operator: '$eq', value: 'cat-news' }],
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
it('does NOT populate any top-level scalar slot for `id`', async () => {
|
|
677
|
+
// Sanity: unlike status / pathFilter, `id` has no top-level scalar
|
|
678
|
+
// form on `ParsedWhere`. Always lives in `filters[]` as a docColumn.
|
|
679
|
+
const result = await parseWhere({ id: 'doc-x' }, testCollection);
|
|
680
|
+
expect(result.status).toBeUndefined();
|
|
681
|
+
expect(result.pathFilter).toBeUndefined();
|
|
682
|
+
expect(result.query).toBeUndefined();
|
|
683
|
+
expect(result.filters).toHaveLength(1);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
628
687
|
// mergePredicates
|
|
629
688
|
// ---------------------------------------------------------------------------
|
|
630
689
|
describe('mergePredicates', () => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "2.5.
|
|
5
|
+
"version": "2.5.2",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"sharp": "^0.34.5",
|
|
80
80
|
"uuid": "^14.0.0",
|
|
81
81
|
"zod": "^4.4.3",
|
|
82
|
-
"@byline/auth": "2.5.
|
|
82
|
+
"@byline/auth": "2.5.2"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|