@byline/core 3.20.2 → 3.20.3

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.
@@ -54,6 +54,22 @@ export interface ColumnDefinition<T = any> {
54
54
  className?: string;
55
55
  formatter?: ColumnFormatter<T>;
56
56
  }
57
+ /**
58
+ * Default sort for a collection's list view, applied when the URL carries
59
+ * no explicit `order`/`desc` search params. An explicit param always wins
60
+ * (a shared link opens exactly as sent).
61
+ *
62
+ * `field` is a top-level schema field name (e.g. `'publicationDate'`) or a
63
+ * document-level column (`'createdAt'`, `'updatedAt'`, `'path'`). Validated
64
+ * at boot by `validateAdminConfigs`. Not allowed on `orderable: true`
65
+ * collections — manual ordering already owns their default sort
66
+ * (`order_key asc`).
67
+ */
68
+ export interface ListDefaultSort<T = any> {
69
+ field: keyof T | 'createdAt' | 'updatedAt' | 'path';
70
+ /** Defaults to `'asc'`. */
71
+ direction?: 'asc' | 'desc';
72
+ }
57
73
  /**
58
74
  * A single tab inside a tab set.
59
75
  */
@@ -181,6 +197,21 @@ export interface CollectionAdminConfig<T = any> {
181
197
  group?: string;
182
198
  /** Column definitions for the collection list view. */
183
199
  columns?: ColumnDefinition<T>[];
200
+ /**
201
+ * Default sort for the list view when the URL carries no explicit
202
+ * `order`/`desc` search params — e.g. a publications library that should
203
+ * open newest-publication-first:
204
+ *
205
+ * ```ts
206
+ * defaultSort: { field: 'publicationDate', direction: 'desc' },
207
+ * ```
208
+ *
209
+ * Precedence: explicit URL params → this default → `created_at desc`.
210
+ * Boot-validated: `field` must be a top-level schema field or a document
211
+ * column (`createdAt` / `updatedAt` / `path`), and the option is rejected
212
+ * on `orderable: true` collections (manual ordering owns their default).
213
+ */
214
+ defaultSort?: ListDefaultSort<T>;
184
215
  /**
185
216
  * Column definitions for rendering this collection as a compact **item
186
217
  * row / tile** wherever a single document is shown outside its own list —
@@ -24,6 +24,11 @@ import type { CollectionAdminConfig, CollectionDefinition } from '../@types/inde
24
24
  * schema field names; groups exclude tabSets and nested groups.
25
25
  * 6. `fields` map sanity — every key in `admin.fields` matches a top-level
26
26
  * schema field name.
27
+ * 7. `defaultSort` sanity — `field` resolves to a top-level schema field
28
+ * or a document-level column (`createdAt` / `updatedAt` / `path`), the
29
+ * direction (when given) is `asc` | `desc`, and the option is rejected
30
+ * on `orderable: true` collections (manual ordering owns their default
31
+ * sort and the drag-to-reorder canonical-view check assumes it).
27
32
  *
28
33
  * Throws a plain `Error` (not a `BylineError`) because configuration
29
34
  * validation runs at startup, before the logger and error registry are
@@ -23,6 +23,11 @@
23
23
  * schema field names; groups exclude tabSets and nested groups.
24
24
  * 6. `fields` map sanity — every key in `admin.fields` matches a top-level
25
25
  * schema field name.
26
+ * 7. `defaultSort` sanity — `field` resolves to a top-level schema field
27
+ * or a document-level column (`createdAt` / `updatedAt` / `path`), the
28
+ * direction (when given) is `asc` | `desc`, and the option is rejected
29
+ * on `orderable: true` collections (manual ordering owns their default
30
+ * sort and the drag-to-reorder canonical-view check assumes it).
26
31
  *
27
32
  * Throws a plain `Error` (not a `BylineError`) because configuration
28
33
  * validation runs at startup, before the logger and error registry are
@@ -57,6 +62,26 @@ function validateOne(admin, collectionsByPath) {
57
62
  if ('name' in field)
58
63
  topLevelFieldNames.add(field.name);
59
64
  }
65
+ // Rule 7 — defaultSort sanity. The list read applies this spec verbatim
66
+ // (host list server fn → parseSort), so a bad field name would silently
67
+ // fall back to `created_at desc` at request time — fail loudly at boot
68
+ // instead.
69
+ if (admin.defaultSort != null) {
70
+ const { field, direction } = admin.defaultSort;
71
+ const documentColumns = new Set(['createdAt', 'updatedAt', 'path']);
72
+ if (collection.orderable === true) {
73
+ fail(`defaultSort is not allowed on an orderable collection — manual ordering owns the default sort (order_key asc).`);
74
+ }
75
+ if (typeof field !== 'string' || field.length === 0) {
76
+ fail(`defaultSort.field must be a non-empty string.`);
77
+ }
78
+ if (!topLevelFieldNames.has(field) && !documentColumns.has(field)) {
79
+ fail(`defaultSort.field "${String(field)}" is not a top-level schema field or a document column (createdAt, updatedAt, path).`);
80
+ }
81
+ if (direction != null && direction !== 'asc' && direction !== 'desc') {
82
+ fail(`defaultSort.direction must be 'asc' or 'desc' (got "${String(direction)}").`);
83
+ }
84
+ }
60
85
  // Build primitive lookup tables.
61
86
  const tabSets = admin.tabSets ?? [];
62
87
  const rows = admin.rows ?? [];
@@ -201,6 +201,43 @@ describe('validateAdminConfigs', () => {
201
201
  };
202
202
  expect(() => validateAdminConfigs([admin], [collection])).toThrow(/not a top-level schema field/);
203
203
  });
204
+ // Rule 7 — defaultSort sanity.
205
+ it('accepts a defaultSort on a schema field', () => {
206
+ const admin = {
207
+ ...baseAdmin,
208
+ defaultSort: { field: 'title', direction: 'desc' },
209
+ };
210
+ expect(() => validateAdminConfigs([admin], [collection])).not.toThrow();
211
+ });
212
+ it('accepts a defaultSort on a document column', () => {
213
+ const admin = {
214
+ ...baseAdmin,
215
+ defaultSort: { field: 'updatedAt' },
216
+ };
217
+ expect(() => validateAdminConfigs([admin], [collection])).not.toThrow();
218
+ });
219
+ it('rejects a defaultSort field that resolves to nothing', () => {
220
+ const admin = {
221
+ ...baseAdmin,
222
+ defaultSort: { field: 'nonexistent' },
223
+ };
224
+ expect(() => validateAdminConfigs([admin], [collection])).toThrow(/defaultSort\.field "nonexistent"/);
225
+ });
226
+ it('rejects a defaultSort direction outside asc/desc', () => {
227
+ const admin = {
228
+ ...baseAdmin,
229
+ defaultSort: { field: 'title', direction: 'sideways' },
230
+ };
231
+ expect(() => validateAdminConfigs([admin], [collection])).toThrow(/defaultSort\.direction must be 'asc' or 'desc'/);
232
+ });
233
+ it('rejects a defaultSort on an orderable collection', () => {
234
+ const orderableCollection = { ...collection, orderable: true };
235
+ const admin = {
236
+ ...baseAdmin,
237
+ defaultSort: { field: 'title' },
238
+ };
239
+ expect(() => validateAdminConfigs([admin], [orderableCollection])).toThrow(/not allowed on an orderable collection/);
240
+ });
204
241
  // Layout omitted — bookkeeping skipped, primitive declarations still checked.
205
242
  it('skips placement bookkeeping when layout is omitted', () => {
206
243
  const admin = {
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": "3.20.2",
5
+ "version": "3.20.3",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -76,7 +76,7 @@
76
76
  "pino": "^10.3.1",
77
77
  "sharp": "^0.35.3",
78
78
  "zod": "^4.4.3",
79
- "@byline/auth": "3.20.2"
79
+ "@byline/auth": "3.20.3"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@biomejs/biome": "2.5.2",