@byline/admin 4.5.0 → 4.6.1

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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Transport-agnostic commands for per-user admin preferences.
10
+ *
11
+ * Same self-service posture as admin-account: `auth` is
12
+ * `{ authenticated: true }` (no ability key), and the security property
13
+ * "you may only touch your own preferences" is structural — the target
14
+ * id comes from `actor.id`, never from the request payload.
15
+ */
16
+ import { type Command } from '../../lib/create-command.js';
17
+ import type { AdminStore } from '../../store.js';
18
+ import type { GetPreferenceRequest, PreferenceResponse, SetPreferenceRequest } from './schemas.js';
19
+ export interface AdminPreferencesCommandDeps {
20
+ store: AdminStore;
21
+ }
22
+ export declare const getPreferenceCommand: Command<GetPreferenceRequest, PreferenceResponse, AdminPreferencesCommandDeps>;
23
+ export declare const setPreferenceCommand: Command<SetPreferenceRequest, PreferenceResponse, AdminPreferencesCommandDeps>;
@@ -0,0 +1,31 @@
1
+ import { createCommand } from "../../lib/create-command.js";
2
+ import { getPreferenceRequestSchema, preferenceResponseSchema, setPreferenceRequestSchema } from "./schemas.js";
3
+ import { AdminPreferencesService } from "./service.js";
4
+ function serviceOf(deps) {
5
+ return new AdminPreferencesService({
6
+ repo: deps.store.adminPreferences
7
+ });
8
+ }
9
+ const getPreferenceCommand = createCommand({
10
+ method: 'getPreference',
11
+ auth: {
12
+ authenticated: true
13
+ },
14
+ schemas: {
15
+ input: getPreferenceRequestSchema,
16
+ output: preferenceResponseSchema
17
+ },
18
+ handler: ({ input, deps, actor })=>serviceOf(deps).getPreference(actor.id, input.scope)
19
+ });
20
+ const setPreferenceCommand = createCommand({
21
+ method: 'setPreference',
22
+ auth: {
23
+ authenticated: true
24
+ },
25
+ schemas: {
26
+ input: setPreferenceRequestSchema,
27
+ output: preferenceResponseSchema
28
+ },
29
+ handler: ({ input, deps, actor })=>serviceOf(deps).setPreference(actor.id, input.scope, input.value)
30
+ });
31
+ export { getPreferenceCommand, setPreferenceCommand };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * `@byline/admin/admin-preferences` — scoped per-user key-value
10
+ * preferences for the currently signed-in admin user.
11
+ *
12
+ * Self-service like `@byline/admin/admin-account`: the actor IS the
13
+ * target, and there is no ability gate — authn-only. The `scope` string
14
+ * (e.g. `collections.docs.list`) is the generality lever: new admin
15
+ * surfaces claim their own scopes with no schema change.
16
+ */
17
+ export { getPreferenceCommand, setPreferenceCommand } from './commands.js';
18
+ export { getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema, } from './schemas.js';
19
+ export { AdminPreferencesService } from './service.js';
20
+ export type { AdminPreferencesCommandDeps } from './commands.js';
21
+ export type { AdminPreferencesRepository, AdminUserPreferenceRow, } from './repository.js';
22
+ export type { GetPreferenceRequest, PreferenceResponse, SetPreferenceRequest, } from './schemas.js';
@@ -0,0 +1,3 @@
1
+ export { getPreferenceCommand, setPreferenceCommand } from "./commands.js";
2
+ export { getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema } from "./schemas.js";
3
+ export { AdminPreferencesService } from "./service.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * `AdminPreferencesRepository` — the DB-adapter-facing contract for the
10
+ * `byline_admin_user_preferences` table: a scoped per-user key-value
11
+ * store. One row per (user, scope); `value` is a JSONB object whose
12
+ * shape is owned by the scope's feature (validated at the command
13
+ * layer, not here).
14
+ *
15
+ * Adapters (e.g. `@byline/db-postgres`) implement this interface; the
16
+ * admin-preferences service consumes it via the `AdminStore` bundle.
17
+ */
18
+ export interface AdminUserPreferenceRow {
19
+ user_id: string;
20
+ scope: string;
21
+ value: Record<string, unknown>;
22
+ created_at: Date;
23
+ updated_at: Date;
24
+ }
25
+ export interface AdminPreferencesRepository {
26
+ /** `null` when the user has no row for the scope. */
27
+ get(userId: string, scope: string): Promise<AdminUserPreferenceRow | null>;
28
+ /**
29
+ * Insert-or-merge. On conflict the JSONB `patch` is merged into the
30
+ * stored value **per key** (`value || patch`), so writing
31
+ * `{ page_size }` preserves a previously stored `order`/`desc`.
32
+ * Vid-less — preferences are last-writer-wins by design.
33
+ */
34
+ upsert(userId: string, scope: string, patch: Record<string, unknown>): Promise<AdminUserPreferenceRow>;
35
+ }
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Zod schemas for the admin-preferences commands.
10
+ *
11
+ * Self-service, like admin-account: none of the request schemas accept a
12
+ * user id — the command resolves the target from `actor.id`.
13
+ *
14
+ * The `value` payload is validated against the list-view shape because
15
+ * that is the only registered scope family today
16
+ * (`collections.<path>.list`). When a second scope family arrives, this
17
+ * becomes a scope-keyed selection of value schemas.
18
+ */
19
+ import { z } from 'zod';
20
+ /** Dot-separated segment key, e.g. `collections.docs.list`. */
21
+ export declare const preferenceScopeSchema: z.ZodString;
22
+ /**
23
+ * Sticky list-view keys. All optional — clients send only the keys the
24
+ * interaction changed, and the repository merges per-key — but an empty
25
+ * object is rejected (nothing to write).
26
+ */
27
+ export declare const listViewPreferenceValueSchema: z.ZodObject<{
28
+ page_size: z.ZodOptional<z.ZodNumber>;
29
+ order: z.ZodOptional<z.ZodString>;
30
+ desc: z.ZodOptional<z.ZodBoolean>;
31
+ }, z.core.$strict>;
32
+ export declare const getPreferenceRequestSchema: z.ZodObject<{
33
+ scope: z.ZodString;
34
+ }, z.core.$strip>;
35
+ export type GetPreferenceRequest = z.infer<typeof getPreferenceRequestSchema>;
36
+ export declare const setPreferenceRequestSchema: z.ZodObject<{
37
+ scope: z.ZodString;
38
+ value: z.ZodObject<{
39
+ page_size: z.ZodOptional<z.ZodNumber>;
40
+ order: z.ZodOptional<z.ZodString>;
41
+ desc: z.ZodOptional<z.ZodBoolean>;
42
+ }, z.core.$strict>;
43
+ }, z.core.$strip>;
44
+ export type SetPreferenceRequest = z.infer<typeof setPreferenceRequestSchema>;
45
+ /** `value` is `null` when the user has no stored preference for the scope. */
46
+ export declare const preferenceResponseSchema: z.ZodObject<{
47
+ scope: z.ZodString;
48
+ value: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
49
+ }, z.core.$strip>;
50
+ export type PreferenceResponse = z.infer<typeof preferenceResponseSchema>;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ const preferenceScopeSchema = z.string().min(1).max(255).regex(/^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/i, {
3
+ message: 'scope must be dot-separated segments of [a-z0-9_-]'
4
+ });
5
+ const listViewPreferenceValueSchema = z.object({
6
+ page_size: z.number().int().min(1).max(100).optional(),
7
+ order: z.string().min(1).max(255).optional(),
8
+ desc: z.boolean().optional()
9
+ }).strict().refine((v)=>Object.keys(v).length > 0, {
10
+ message: 'value cannot be empty'
11
+ });
12
+ const getPreferenceRequestSchema = z.object({
13
+ scope: preferenceScopeSchema
14
+ });
15
+ const setPreferenceRequestSchema = z.object({
16
+ scope: preferenceScopeSchema,
17
+ value: listViewPreferenceValueSchema
18
+ });
19
+ const preferenceResponseSchema = z.object({
20
+ scope: z.string(),
21
+ value: z.record(z.string(), z.unknown()).nullable()
22
+ });
23
+ export { getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Self-service business logic for per-user admin preferences. Every
10
+ * method takes `actorId` sourced server-side from the authenticated
11
+ * `RequestContext` — callers cannot supply a target user id.
12
+ */
13
+ import type { AdminPreferencesRepository } from './repository.js';
14
+ import type { PreferenceResponse } from './schemas.js';
15
+ export declare class AdminPreferencesService {
16
+ #private;
17
+ constructor(deps: {
18
+ repo: AdminPreferencesRepository;
19
+ });
20
+ getPreference(actorId: string, scope: string): Promise<PreferenceResponse>;
21
+ setPreference(actorId: string, scope: string, patch: Record<string, unknown>): Promise<PreferenceResponse>;
22
+ }
@@ -0,0 +1,56 @@
1
+ function _check_private_redeclaration(obj, privateCollection) {
2
+ if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
3
+ }
4
+ function _class_apply_descriptor_get(receiver, descriptor) {
5
+ if (descriptor.get) return descriptor.get.call(receiver);
6
+ return descriptor.value;
7
+ }
8
+ function _class_apply_descriptor_set(receiver, descriptor, value) {
9
+ if (descriptor.set) descriptor.set.call(receiver, value);
10
+ else {
11
+ if (!descriptor.writable) throw new TypeError("attempted to set read only private field");
12
+ descriptor.value = value;
13
+ }
14
+ }
15
+ function _class_extract_field_descriptor(receiver, privateMap, action) {
16
+ if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
17
+ return privateMap.get(receiver);
18
+ }
19
+ function _class_private_field_get(receiver, privateMap) {
20
+ var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
21
+ return _class_apply_descriptor_get(receiver, descriptor);
22
+ }
23
+ function _class_private_field_init(obj, privateMap, value) {
24
+ _check_private_redeclaration(obj, privateMap);
25
+ privateMap.set(obj, value);
26
+ }
27
+ function _class_private_field_set(receiver, privateMap, value) {
28
+ var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set");
29
+ _class_apply_descriptor_set(receiver, descriptor, value);
30
+ return value;
31
+ }
32
+ var _repo = /*#__PURE__*/ new WeakMap();
33
+ class AdminPreferencesService {
34
+ async getPreference(actorId, scope) {
35
+ const row = await _class_private_field_get(this, _repo).get(actorId, scope);
36
+ return {
37
+ scope,
38
+ value: row?.value ?? null
39
+ };
40
+ }
41
+ async setPreference(actorId, scope, patch) {
42
+ const row = await _class_private_field_get(this, _repo).upsert(actorId, scope, patch);
43
+ return {
44
+ scope,
45
+ value: row.value
46
+ };
47
+ }
48
+ constructor(deps){
49
+ _class_private_field_init(this, _repo, {
50
+ writable: true,
51
+ value: void 0
52
+ });
53
+ _class_private_field_set(this, _repo, deps.repo);
54
+ }
55
+ }
56
+ export { AdminPreferencesService };
package/dist/store.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { AdminPermissionsRepository } from './modules/admin-permissions/repository.js';
9
+ import type { AdminPreferencesRepository } from './modules/admin-preferences/repository.js';
9
10
  import type { AdminRolesRepository } from './modules/admin-roles/repository.js';
10
11
  import type { AdminUsersRepository } from './modules/admin-users/repository.js';
11
12
  import type { RefreshTokensRepository } from './modules/auth/refresh-tokens-repository.js';
@@ -18,7 +19,7 @@ import type { RefreshTokensRepository } from './modules/auth/refresh-tokens-repo
18
19
  * `JwtSessionProvider`, to `seedSuperAdmin`, and (later) to admin-user
19
20
  * and admin-role commands.
20
21
  *
21
- * Keeping the four repositories together as a single argument avoids
22
+ * Keeping the five repositories together as a single argument avoids
22
23
  * exploding constructor signatures and makes "needs admin DB access" a
23
24
  * single, recognisable type.
24
25
  */
@@ -27,4 +28,5 @@ export interface AdminStore {
27
28
  adminRoles: AdminRolesRepository;
28
29
  adminPermissions: AdminPermissionsRepository;
29
30
  refreshTokens: RefreshTokensRepository;
31
+ adminPreferences: AdminPreferencesRepository;
30
32
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "4.5.0",
5
+ "version": "4.6.1",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -72,6 +72,11 @@
72
72
  "import": "./dist/modules/admin-account/index.js",
73
73
  "require": "./dist/modules/admin-account/index.js"
74
74
  },
75
+ "./admin-preferences": {
76
+ "types": "./dist/modules/admin-preferences/index.d.ts",
77
+ "import": "./dist/modules/admin-preferences/index.js",
78
+ "require": "./dist/modules/admin-preferences/index.js"
79
+ },
75
80
  "./services": {
76
81
  "types": "./dist/services/admin-services-context.d.ts",
77
82
  "import": "./dist/services/admin-services-context.js",
@@ -168,10 +173,10 @@
168
173
  "react-diff-viewer-continued": "^4.4.0",
169
174
  "uuid": "^14.0.1",
170
175
  "zod": "^4.4.3",
171
- "@byline/auth": "4.5.0",
172
- "@byline/ui": "4.5.0",
173
- "@byline/i18n": "4.5.0",
174
- "@byline/core": "4.5.0"
176
+ "@byline/i18n": "4.6.1",
177
+ "@byline/ui": "4.6.1",
178
+ "@byline/core": "4.6.1",
179
+ "@byline/auth": "4.6.1"
175
180
  },
176
181
  "peerDependencies": {
177
182
  "react": "^19.0.0",
@@ -0,0 +1,57 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Transport-agnostic commands for per-user admin preferences.
11
+ *
12
+ * Same self-service posture as admin-account: `auth` is
13
+ * `{ authenticated: true }` (no ability key), and the security property
14
+ * "you may only touch your own preferences" is structural — the target
15
+ * id comes from `actor.id`, never from the request payload.
16
+ */
17
+
18
+ import { type Command, createCommand } from '../../lib/create-command.js'
19
+ import {
20
+ getPreferenceRequestSchema,
21
+ preferenceResponseSchema,
22
+ setPreferenceRequestSchema,
23
+ } from './schemas.js'
24
+ import { AdminPreferencesService } from './service.js'
25
+ import type { AdminStore } from '../../store.js'
26
+ import type { GetPreferenceRequest, PreferenceResponse, SetPreferenceRequest } from './schemas.js'
27
+
28
+ export interface AdminPreferencesCommandDeps {
29
+ store: AdminStore
30
+ }
31
+
32
+ function serviceOf(deps: AdminPreferencesCommandDeps): AdminPreferencesService {
33
+ return new AdminPreferencesService({ repo: deps.store.adminPreferences })
34
+ }
35
+
36
+ export const getPreferenceCommand: Command<
37
+ GetPreferenceRequest,
38
+ PreferenceResponse,
39
+ AdminPreferencesCommandDeps
40
+ > = createCommand({
41
+ method: 'getPreference',
42
+ auth: { authenticated: true },
43
+ schemas: { input: getPreferenceRequestSchema, output: preferenceResponseSchema },
44
+ handler: ({ input, deps, actor }) => serviceOf(deps).getPreference(actor.id, input.scope),
45
+ })
46
+
47
+ export const setPreferenceCommand: Command<
48
+ SetPreferenceRequest,
49
+ PreferenceResponse,
50
+ AdminPreferencesCommandDeps
51
+ > = createCommand({
52
+ method: 'setPreference',
53
+ auth: { authenticated: true },
54
+ schemas: { input: setPreferenceRequestSchema, output: preferenceResponseSchema },
55
+ handler: ({ input, deps, actor }) =>
56
+ serviceOf(deps).setPreference(actor.id, input.scope, input.value),
57
+ })
@@ -0,0 +1,37 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * `@byline/admin/admin-preferences` — scoped per-user key-value
11
+ * preferences for the currently signed-in admin user.
12
+ *
13
+ * Self-service like `@byline/admin/admin-account`: the actor IS the
14
+ * target, and there is no ability gate — authn-only. The `scope` string
15
+ * (e.g. `collections.docs.list`) is the generality lever: new admin
16
+ * surfaces claim their own scopes with no schema change.
17
+ */
18
+
19
+ export { getPreferenceCommand, setPreferenceCommand } from './commands.js'
20
+ export {
21
+ getPreferenceRequestSchema,
22
+ listViewPreferenceValueSchema,
23
+ preferenceResponseSchema,
24
+ preferenceScopeSchema,
25
+ setPreferenceRequestSchema,
26
+ } from './schemas.js'
27
+ export { AdminPreferencesService } from './service.js'
28
+ export type { AdminPreferencesCommandDeps } from './commands.js'
29
+ export type {
30
+ AdminPreferencesRepository,
31
+ AdminUserPreferenceRow,
32
+ } from './repository.js'
33
+ export type {
34
+ GetPreferenceRequest,
35
+ PreferenceResponse,
36
+ SetPreferenceRequest,
37
+ } from './schemas.js'
@@ -0,0 +1,42 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * `AdminPreferencesRepository` — the DB-adapter-facing contract for the
11
+ * `byline_admin_user_preferences` table: a scoped per-user key-value
12
+ * store. One row per (user, scope); `value` is a JSONB object whose
13
+ * shape is owned by the scope's feature (validated at the command
14
+ * layer, not here).
15
+ *
16
+ * Adapters (e.g. `@byline/db-postgres`) implement this interface; the
17
+ * admin-preferences service consumes it via the `AdminStore` bundle.
18
+ */
19
+
20
+ export interface AdminUserPreferenceRow {
21
+ user_id: string
22
+ scope: string
23
+ value: Record<string, unknown>
24
+ created_at: Date
25
+ updated_at: Date
26
+ }
27
+
28
+ export interface AdminPreferencesRepository {
29
+ /** `null` when the user has no row for the scope. */
30
+ get(userId: string, scope: string): Promise<AdminUserPreferenceRow | null>
31
+ /**
32
+ * Insert-or-merge. On conflict the JSONB `patch` is merged into the
33
+ * stored value **per key** (`value || patch`), so writing
34
+ * `{ page_size }` preserves a previously stored `order`/`desc`.
35
+ * Vid-less — preferences are last-writer-wins by design.
36
+ */
37
+ upsert(
38
+ userId: string,
39
+ scope: string,
40
+ patch: Record<string, unknown>
41
+ ): Promise<AdminUserPreferenceRow>
42
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { describe, expect, it } from 'vitest'
10
+
11
+ import {
12
+ listViewPreferenceValueSchema,
13
+ preferenceScopeSchema,
14
+ setPreferenceRequestSchema,
15
+ } from './schemas.js'
16
+
17
+ describe('preferenceScopeSchema', () => {
18
+ it('accepts dot-separated scope keys', () => {
19
+ expect(preferenceScopeSchema.safeParse('collections.docs.list').success).toBe(true)
20
+ expect(preferenceScopeSchema.safeParse('collections.media-items.list').success).toBe(true)
21
+ })
22
+
23
+ it('rejects empty, spaced, and slash-separated keys', () => {
24
+ expect(preferenceScopeSchema.safeParse('').success).toBe(false)
25
+ expect(preferenceScopeSchema.safeParse('has space').success).toBe(false)
26
+ expect(preferenceScopeSchema.safeParse('a/b').success).toBe(false)
27
+ })
28
+ })
29
+
30
+ describe('listViewPreferenceValueSchema', () => {
31
+ it('accepts a page_size-only payload (partial writes are the norm)', () => {
32
+ expect(listViewPreferenceValueSchema.safeParse({ page_size: 50 }).success).toBe(true)
33
+ })
34
+
35
+ it('accepts a sort-only payload', () => {
36
+ expect(listViewPreferenceValueSchema.safeParse({ order: 'title', desc: true }).success).toBe(
37
+ true
38
+ )
39
+ })
40
+
41
+ it('enforces the 1-100 page_size bounds', () => {
42
+ expect(listViewPreferenceValueSchema.safeParse({ page_size: 0 }).success).toBe(false)
43
+ expect(listViewPreferenceValueSchema.safeParse({ page_size: 101 }).success).toBe(false)
44
+ expect(listViewPreferenceValueSchema.safeParse({ page_size: 12.5 }).success).toBe(false)
45
+ expect(listViewPreferenceValueSchema.safeParse({ page_size: 1 }).success).toBe(true)
46
+ expect(listViewPreferenceValueSchema.safeParse({ page_size: 100 }).success).toBe(true)
47
+ })
48
+
49
+ it('rejects an empty payload and unknown keys', () => {
50
+ expect(listViewPreferenceValueSchema.safeParse({}).success).toBe(false)
51
+ expect(listViewPreferenceValueSchema.safeParse({ page: 7 }).success).toBe(false)
52
+ })
53
+ })
54
+
55
+ describe('setPreferenceRequestSchema', () => {
56
+ it('requires both scope and a non-empty value', () => {
57
+ expect(
58
+ setPreferenceRequestSchema.safeParse({
59
+ scope: 'collections.docs.list',
60
+ value: { page_size: 30 },
61
+ }).success
62
+ ).toBe(true)
63
+ expect(
64
+ setPreferenceRequestSchema.safeParse({ scope: 'collections.docs.list', value: {} }).success
65
+ ).toBe(false)
66
+ })
67
+ })
@@ -0,0 +1,62 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Zod schemas for the admin-preferences commands.
11
+ *
12
+ * Self-service, like admin-account: none of the request schemas accept a
13
+ * user id — the command resolves the target from `actor.id`.
14
+ *
15
+ * The `value` payload is validated against the list-view shape because
16
+ * that is the only registered scope family today
17
+ * (`collections.<path>.list`). When a second scope family arrives, this
18
+ * becomes a scope-keyed selection of value schemas.
19
+ */
20
+
21
+ import { z } from 'zod'
22
+
23
+ /** Dot-separated segment key, e.g. `collections.docs.list`. */
24
+ export const preferenceScopeSchema = z
25
+ .string()
26
+ .min(1)
27
+ .max(255)
28
+ .regex(/^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/i, {
29
+ message: 'scope must be dot-separated segments of [a-z0-9_-]',
30
+ })
31
+
32
+ /**
33
+ * Sticky list-view keys. All optional — clients send only the keys the
34
+ * interaction changed, and the repository merges per-key — but an empty
35
+ * object is rejected (nothing to write).
36
+ */
37
+ export const listViewPreferenceValueSchema = z
38
+ .object({
39
+ page_size: z.number().int().min(1).max(100).optional(),
40
+ order: z.string().min(1).max(255).optional(),
41
+ desc: z.boolean().optional(),
42
+ })
43
+ .strict()
44
+ .refine((v) => Object.keys(v).length > 0, { message: 'value cannot be empty' })
45
+
46
+ export const getPreferenceRequestSchema = z.object({
47
+ scope: preferenceScopeSchema,
48
+ })
49
+ export type GetPreferenceRequest = z.infer<typeof getPreferenceRequestSchema>
50
+
51
+ export const setPreferenceRequestSchema = z.object({
52
+ scope: preferenceScopeSchema,
53
+ value: listViewPreferenceValueSchema,
54
+ })
55
+ export type SetPreferenceRequest = z.infer<typeof setPreferenceRequestSchema>
56
+
57
+ /** `value` is `null` when the user has no stored preference for the scope. */
58
+ export const preferenceResponseSchema = z.object({
59
+ scope: z.string(),
60
+ value: z.record(z.string(), z.unknown()).nullable(),
61
+ })
62
+ export type PreferenceResponse = z.infer<typeof preferenceResponseSchema>
@@ -0,0 +1,38 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Self-service business logic for per-user admin preferences. Every
11
+ * method takes `actorId` sourced server-side from the authenticated
12
+ * `RequestContext` — callers cannot supply a target user id.
13
+ */
14
+
15
+ import type { AdminPreferencesRepository } from './repository.js'
16
+ import type { PreferenceResponse } from './schemas.js'
17
+
18
+ export class AdminPreferencesService {
19
+ readonly #repo: AdminPreferencesRepository
20
+
21
+ constructor(deps: { repo: AdminPreferencesRepository }) {
22
+ this.#repo = deps.repo
23
+ }
24
+
25
+ async getPreference(actorId: string, scope: string): Promise<PreferenceResponse> {
26
+ const row = await this.#repo.get(actorId, scope)
27
+ return { scope, value: row?.value ?? null }
28
+ }
29
+
30
+ async setPreference(
31
+ actorId: string,
32
+ scope: string,
33
+ patch: Record<string, unknown>
34
+ ): Promise<PreferenceResponse> {
35
+ const row = await this.#repo.upsert(actorId, scope, patch)
36
+ return { scope, value: row.value }
37
+ }
38
+ }
package/src/store.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { AdminPermissionsRepository } from './modules/admin-permissions/repository.js'
10
+ import type { AdminPreferencesRepository } from './modules/admin-preferences/repository.js'
10
11
  import type { AdminRolesRepository } from './modules/admin-roles/repository.js'
11
12
  import type { AdminUsersRepository } from './modules/admin-users/repository.js'
12
13
  import type { RefreshTokensRepository } from './modules/auth/refresh-tokens-repository.js'
@@ -20,7 +21,7 @@ import type { RefreshTokensRepository } from './modules/auth/refresh-tokens-repo
20
21
  * `JwtSessionProvider`, to `seedSuperAdmin`, and (later) to admin-user
21
22
  * and admin-role commands.
22
23
  *
23
- * Keeping the four repositories together as a single argument avoids
24
+ * Keeping the five repositories together as a single argument avoids
24
25
  * exploding constructor signatures and makes "needs admin DB access" a
25
26
  * single, recognisable type.
26
27
  */
@@ -29,4 +30,5 @@ export interface AdminStore {
29
30
  adminRoles: AdminRolesRepository
30
31
  adminPermissions: AdminPermissionsRepository
31
32
  refreshTokens: RefreshTokensRepository
33
+ adminPreferences: AdminPreferencesRepository
32
34
  }