@frigear-nu/preferences 0.1.1 → 0.1.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.
package/dist/module.d.mts CHANGED
@@ -15,6 +15,8 @@ type ModuleOptions = {
15
15
  */
16
16
  interface UserPreferences {
17
17
  }
18
+ /** The response of `PATCH /api/preferences`: the preferences now stored for the caller. */
19
+ type PreferencesDto = UserPreferences;
18
20
 
19
21
  declare module 'nuxt/schema' {
20
22
  interface NuxtConfig {
@@ -24,4 +26,4 @@ declare module 'nuxt/schema' {
24
26
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
25
27
 
26
28
  export { _default as default };
27
- export type { ModuleOptions, UserPreferences };
29
+ export type { ModuleOptions, PreferencesDto, UserPreferences };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/preferences",
3
3
  "configKey": "preferences",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { defineNuxtModule, createResolver, addServerTemplate, addTypeTemplate, addServerImportsDir, addImports, addServerHandler, hasNuxtModule, addServerPlugin } from '@nuxt/kit';
4
- import { registerModule, extendDatabaseTable, inlineModuleRuntime } from '@frigear-nu/module-kit/utils';
3
+ import { defineNuxtModule, createResolver, addServerTemplate, addTypeTemplate, addServerImportsDir, addImports, hasNuxtModule, addServerPlugin } from '@nuxt/kit';
4
+ import { registerModule, extendDatabaseTable, inlineModuleRuntime, addModuleRoutes } from '@frigear-nu/module-kit/utils';
5
5
 
6
6
  const module$1 = defineNuxtModule({
7
7
  meta: { name: "@frigear-nu/preferences", configKey: "preferences" },
@@ -67,11 +67,9 @@ declare module '#preferences/definitions' {
67
67
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
68
68
  inlineModuleRuntime(nuxt, resolver);
69
69
  addImports({ name: "useUserPreferences", from: resolver.resolve("./runtime/app/composables/useUserPreferences") });
70
- addServerHandler({
71
- route: "/api/preferences",
72
- method: "patch",
73
- handler: resolver.resolve("./runtime/server/routes/preferences.patch")
74
- });
70
+ addModuleRoutes(resolver, [
71
+ { method: "patch", path: "", file: "preferences.patch" }
72
+ ], { base: "/api/preferences" });
75
73
  if (hasNuxtModule("@frigear-nu/oauth-server")) {
76
74
  addServerPlugin(resolver.resolve("./runtime/server/plugins/oauth-claims"));
77
75
  }
@@ -1,3 +1,4 @@
1
+ import { preferencesApi } from "../../utils/api.js";
1
2
  export const useUserPreferences = () => {
2
3
  const { user, fetch: refreshSession } = useUserSession();
3
4
  const pending = ref(false);
@@ -8,7 +9,7 @@ export const useUserPreferences = () => {
8
9
  async function set(key, value) {
9
10
  pending.value = true;
10
11
  try {
11
- await $fetch("/api/preferences", { method: "PATCH", body: { [key]: value } });
12
+ await $fetch(preferencesApi.root, { method: "PATCH", body: { [key]: value } });
12
13
  await refreshSession();
13
14
  } finally {
14
15
  pending.value = false;
@@ -1,17 +1,15 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { eq } from "drizzle-orm";
3
- import { parsePreferencePatch } from "../../utils/registry.js";
3
+ import { notFound } from "@frigear-nu/module-kit/server";
4
+ import { applyPreferencePatch, parsePreferencePatch } from "../../utils/registry.js";
4
5
  export default defineEventHandler(async (event) => {
5
6
  const user = await requireUser(event, { sessionOnly: true });
6
- const patch = parsePreferencePatch(getPreferenceRegistry(), await readBody(event));
7
- if (!patch.ok) throw createError({ statusCode: 400, statusMessage: patch.message });
7
+ const patch = await readValidatedBody(event, (body) => parsePreferencePatch(getPreferenceRegistry(), body));
8
8
  const [current] = await db.select({ preferences: schema.users.preferences }).from(schema.users).where(eq(schema.users.id, user.id)).limit(1);
9
- if (!current) throw createError({ statusCode: 404, statusMessage: "User not found." });
10
- const preferences = Object.fromEntries(
11
- Object.entries({ ...current.preferences, ...patch.set }).filter(([key]) => !patch.remove.includes(key))
12
- );
9
+ if (!current) throw notFound("User not found.");
10
+ const preferences = applyPreferencePatch(current.preferences, patch);
13
11
  const [updated] = await db.update(schema.users).set({ preferences }).where(eq(schema.users.id, user.id)).returning();
14
- if (!updated) throw createError({ statusCode: 404, statusMessage: "User not found." });
12
+ if (!updated) throw notFound("User not found.");
15
13
  await setUserSession(event, { user: mapUserToSession(updated) });
16
14
  return preferences;
17
15
  });
@@ -0,0 +1,5 @@
1
+ /** The preferences API. */
2
+ export declare const preferencesApi: {
3
+ /** `PATCH { [key]: value | null }` (session only): sets (or clears with `null`) preferences, answers the stored preferences. */
4
+ readonly root: "/api/preferences";
5
+ };
@@ -0,0 +1,4 @@
1
+ export const preferencesApi = {
2
+ /** `PATCH { [key]: value | null }` (session only): sets (or clears with `null`) preferences, answers the stored preferences. */
3
+ root: "/api/preferences"
4
+ };
@@ -15,3 +15,4 @@ declare module '@nuxt/schema' {
15
15
  * addUserPreferences(nuxt, { path: resolver.resolve('./runtime/preferences') })
16
16
  */
17
17
  export declare function addUserPreferences(nuxt: Nuxt, source: PreferenceDefinitionSource): void;
18
+ export { preferencesApi } from './api.js';
@@ -3,3 +3,4 @@ export function addUserPreferences(nuxt, source) {
3
3
  sources.push(source);
4
4
  });
5
5
  }
6
+ export { preferencesApi } from "./api.js";
@@ -1,4 +1,4 @@
1
- import type { z } from 'zod';
1
+ import { z } from 'zod';
2
2
  import type { PreferenceDefinitions } from '../define.js';
3
3
  export interface ResolvedPreference {
4
4
  key: string;
@@ -10,19 +10,20 @@ export interface ResolvedPreference {
10
10
  }
11
11
  /** Flattens the registered definer files into one lookup. The same key in two files is a mistake worth failing on. */
12
12
  export declare function resolvePreferences(sources: PreferenceDefinitions[]): Map<string, ResolvedPreference>;
13
- export type PreferencePatchResult = {
14
- ok: true;
15
- set: Record<string, unknown>;
16
- remove: string[];
17
- } | {
18
- ok: false;
19
- message: string;
20
- };
21
13
  /**
22
- * Validates a partial update: every key must be registered, a value must satisfy its schema, and `null` clears
23
- * the preference.
14
+ * The zod schema of a partial update, built from the registered definitions: a strict object, so an unknown key is
15
+ * rejected, whose values satisfy their definition's schema or are `null` (which clears the preference).
24
16
  */
25
- export declare function parsePreferencePatch(registry: Map<string, ResolvedPreference>, body: unknown): PreferencePatchResult;
17
+ export declare function preferencePatchSchema(registry: Map<string, ResolvedPreference>): z.ZodObject<{
18
+ [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
19
+ }, z.core.$strict>;
20
+ /**
21
+ * Validates a partial update with `preferencePatchSchema` and keeps only the keys the caller sent: a definition with
22
+ * a `.default()` must not be written just because the key was left out. Throws a `ZodError` (h3 answers 400).
23
+ */
24
+ export declare function parsePreferencePatch(registry: Map<string, ResolvedPreference>, body: unknown): Record<string, unknown>;
25
+ /** Applies a validated patch to the stored preferences: values set, `null` clears. */
26
+ export declare function applyPreferencePatch(stored: Record<string, unknown> | null | undefined, patch: Record<string, unknown>): Record<string, unknown>;
26
27
  /** The stored value when it still satisfies the schema, so a changed schema never leaks stale data. */
27
28
  export declare function readPreference(registry: Map<string, ResolvedPreference>, stored: unknown, key: string): unknown;
28
29
  /** The claims to add for a token: only preferences that opted in, only when the scope was granted. */
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  const isSchema = (value) => typeof value?.safeParse === "function";
2
3
  export function resolvePreferences(sources) {
3
4
  const registry = /* @__PURE__ */ new Map();
@@ -10,22 +11,18 @@ export function resolvePreferences(sources) {
10
11
  }
11
12
  return registry;
12
13
  }
14
+ export function preferencePatchSchema(registry) {
15
+ const shape = {};
16
+ for (const [key, definition] of registry) shape[key] = definition.schema.nullable().optional();
17
+ return z.strictObject(shape);
18
+ }
13
19
  export function parsePreferencePatch(registry, body) {
14
- if (!body || typeof body !== "object" || Array.isArray(body)) return { ok: false, message: "Expected an object of preferences." };
15
- const set = {};
16
- const remove = [];
17
- for (const [key, value] of Object.entries(body)) {
18
- const definition = registry.get(key);
19
- if (!definition) return { ok: false, message: `Unknown preference "${key}".` };
20
- if (value === null) {
21
- remove.push(key);
22
- continue;
23
- }
24
- const parsed = definition.schema.safeParse(value);
25
- if (!parsed.success) return { ok: false, message: `Invalid value for "${key}".` };
26
- set[key] = parsed.data;
27
- }
28
- return { ok: true, set, remove };
20
+ const parsed = preferencePatchSchema(registry).parse(body);
21
+ return Object.fromEntries(Object.keys(body).map((key) => [key, parsed[key]]));
22
+ }
23
+ export function applyPreferencePatch(stored, patch) {
24
+ const next = { ...stored, ...patch };
25
+ return Object.fromEntries(Object.entries(next).filter(([, value]) => value !== null && value !== void 0));
29
26
  }
30
27
  export function readPreference(registry, stored, key) {
31
28
  const definition = registry.get(key);
package/dist/types.d.mts CHANGED
@@ -2,4 +2,4 @@ export { type PreferenceDefinition, type PreferenceDefinitions, type Preferences
2
2
 
3
3
  export { default } from './module.mjs'
4
4
 
5
- export { type ModuleOptions, type UserPreferences } from './module.mjs'
5
+ export { type ModuleOptions, type PreferencesDto, type UserPreferences } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/preferences",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Typed per-user preferences that modules and the host app register, stored on the user and optionally forwarded to OAuth applications as claims.",
5
5
  "homepage": "https://docs.frigear.dev/modules/preferences",
6
6
  "repository": {
@@ -20,6 +20,11 @@
20
20
  "types": "./dist/runtime/utils/build.d.ts",
21
21
  "import": "./dist/runtime/utils/build.js"
22
22
  },
23
+ "./api": {
24
+ "types": "./dist/runtime/utils/api.d.ts",
25
+ "import": "./dist/runtime/utils/api.js",
26
+ "default": "./dist/runtime/utils/api.js"
27
+ },
23
28
  "./define": {
24
29
  "types": "./dist/runtime/define.d.ts",
25
30
  "import": "./dist/runtime/define.js"
@@ -29,9 +34,9 @@
29
34
  "dist"
30
35
  ],
31
36
  "dependencies": {
32
- "@frigear-nu/auth": "0.1.2",
33
- "@frigear-nu/module-kit": "1.1.1",
34
- "@frigear-nu/users": "1.1.2",
37
+ "@frigear-nu/auth": "0.1.4",
38
+ "@frigear-nu/module-kit": "1.1.3",
39
+ "@frigear-nu/users": "1.1.4",
35
40
  "@libsql/client": "^0.18.0",
36
41
  "@nuxt/kit": "^4.6.1",
37
42
  "drizzle-kit": "^0.31.11",
@@ -48,7 +53,7 @@
48
53
  }
49
54
  },
50
55
  "devDependencies": {
51
- "@frigear-nu/oauth-server": "0.1.1",
56
+ "@frigear-nu/oauth-server": "0.1.3",
52
57
  "@nuxt/devtools": "^3.4.2",
53
58
  "@nuxt/eslint-config": "^1.17.0",
54
59
  "@nuxt/module-builder": "^1.0.3",