@murumets-ee/settings 0.11.0 → 0.13.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/dist/admin.d.mts +24 -3
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/admin.mjs.map +1 -1
- package/dist/client-factory-B8jGDCJn.mjs +2 -0
- package/dist/client-factory-B8jGDCJn.mjs.map +1 -0
- package/dist/{client-factory-BwJW9_zW.d.mts → client-factory-BTSJWdrk.d.mts} +19 -8
- package/dist/client-factory-BTSJWdrk.d.mts.map +1 -0
- package/dist/client-factory-C44mk_oR.mjs +2 -0
- package/dist/client-factory-C44mk_oR.mjs.map +1 -0
- package/dist/define-settings-DHaHn6XA.mjs +2 -0
- package/dist/define-settings-DHaHn6XA.mjs.map +1 -0
- package/dist/{define-settings-YygzoniQ.d.mts → define-settings-DvZ1SBIP.d.mts} +12 -9
- package/dist/define-settings-DvZ1SBIP.d.mts.map +1 -0
- package/dist/define.d.mts +3 -3
- package/dist/define.mjs +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/plugin.d.mts +6 -14
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +1 -1
- package/dist/schema.d.mts +5 -5
- package/dist/schema.d.mts.map +1 -1
- package/dist/schema.mjs +1 -1
- package/dist/schema.mjs.map +1 -1
- package/dist/{types-DJ3nKzDt.d.mts → types-C_SUmm7Q.d.mts} +37 -2
- package/dist/types-C_SUmm7Q.d.mts.map +1 -0
- package/dist/view-state.d.mts +3 -3
- package/dist/view-state.d.mts.map +1 -1
- package/dist/view-state.mjs +1 -1
- package/dist/view-state.mjs.map +1 -1
- package/package.json +7 -7
- package/dist/client-factory-BLDPF2zz.mjs +0 -2
- package/dist/client-factory-BLDPF2zz.mjs.map +0 -1
- package/dist/client-factory-BwJW9_zW.d.mts.map +0 -1
- package/dist/client-factory-DBlcuyG0.mjs +0 -2
- package/dist/client-factory-DBlcuyG0.mjs.map +0 -1
- package/dist/define-settings-Bi3STtAH.mjs +0 -2
- package/dist/define-settings-Bi3STtAH.mjs.map +0 -1
- package/dist/define-settings-YygzoniQ.d.mts.map +0 -1
- package/dist/types-DJ3nKzDt.d.mts.map +0 -1
package/dist/admin.d.mts
CHANGED
|
@@ -40,6 +40,18 @@ interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {
|
|
|
40
40
|
default?: T;
|
|
41
41
|
/** Optional Zod schema for validation. If provided, values are validated on set. */
|
|
42
42
|
schema?: ZodType<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Optional renderer slot key. When set, the settings form looks up
|
|
45
|
+
* `renderers[renderer]` (provided by the admin shell) and renders that
|
|
46
|
+
* component in place of the default JSON textarea. Falls back to the
|
|
47
|
+
* JSON renderer if the slot key is absent from the renderer map.
|
|
48
|
+
*
|
|
49
|
+
* The component receives `{ value, onChange, error }` and is responsible
|
|
50
|
+
* for editing the field's value. Convention: prefix with the namespace
|
|
51
|
+
* (e.g. `'media.imageStyles'`, `'commerce.searchRegistry'`) to avoid
|
|
52
|
+
* collisions across plugins.
|
|
53
|
+
*/
|
|
54
|
+
renderer?: string;
|
|
43
55
|
}
|
|
44
56
|
interface MediaSettingConfig extends BaseSettingConfig {
|
|
45
57
|
type: 'media';
|
|
@@ -81,12 +93,21 @@ declare function settingsResources(definitions: SettingsDefinition[]): Array<{
|
|
|
81
93
|
/**
|
|
82
94
|
* Create admin API routes for settings management.
|
|
83
95
|
*
|
|
84
|
-
* Accepts a single definition
|
|
85
|
-
*
|
|
86
|
-
*
|
|
96
|
+
* Accepts a single definition, an array, or a thunk returning the
|
|
97
|
+
* current definitions list. The thunk form lets the settings plugin
|
|
98
|
+
* aggregate contributions from other plugins at init time — its init
|
|
99
|
+
* hook walks `app.plugins.all()`, pulls each plugin's `shared.settings`,
|
|
100
|
+
* and pushes them into a mutable list, which the route handler reads
|
|
101
|
+
* lazily on first request (after init has run).
|
|
102
|
+
*
|
|
103
|
+
* Each namespace gets its own permission resource (`settings:<namespace>`).
|
|
104
|
+
* The merge engine in `@murumets-ee/core` auto-derives that resource and
|
|
105
|
+
* the sidebar entry from `Plugin.shared.settings`, so plugin authors only
|
|
106
|
+
* need to declare the namespace once.
|
|
87
107
|
*/
|
|
88
108
|
declare function settingsRoutes(definition: SettingsDefinition): AdminRoute;
|
|
89
109
|
declare function settingsRoutes(definitions: SettingsDefinition[]): AdminRoute;
|
|
110
|
+
declare function settingsRoutes(getDefinitions: () => SettingsDefinition[]): AdminRoute;
|
|
90
111
|
//#endregion
|
|
91
112
|
export { settingsResources, settingsRoutes };
|
|
92
113
|
//# sourceMappingURL=admin.d.mts.map
|
package/dist/admin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/types.ts","../src/admin/resources.ts","../src/admin/routes.ts"],"mappings":";;;;UAgBiB,iBAAA;EAMH;EAJZ,KAAA;EAOe;EALf,WAAA;;EAEA,YAAA;AAAA;AAAA,UAGe,iBAAA,SAA0B,iBAAA;EACzC,IAAA;EACA,OAAA;EACA,SAAA;EACA,SAAA;EACA,OAAA,GAAU,MAAA;EAEV;EAAA,SAAA;AAAA;AAAA,UAGe,mBAAA,SAA4B,iBAAA;EAC3C,IAAA;EACA,OAAA;EACA,GAAA;EACA,GAAA;EACA,OAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,iBAAA;EAC5C,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,mBAAA,0DACP,iBAAA;EACR,IAAA;EACA,OAAA,EAAS,CAAA;EACT,OAAA,GAAU,CAAA;AAAA;AAAA,UAGK,iBAAA,sBAAuC,iBAAA;EACtD,IAAA;EACA,OAAA,GAAU,CAAA;EAZH;EAcP,MAAA,GAAS,OAAA,CAAQ,CAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/types.ts","../src/admin/resources.ts","../src/admin/routes.ts"],"mappings":";;;;UAgBiB,iBAAA;EAMH;EAJZ,KAAA;EAOe;EALf,WAAA;;EAEA,YAAA;AAAA;AAAA,UAGe,iBAAA,SAA0B,iBAAA;EACzC,IAAA;EACA,OAAA;EACA,SAAA;EACA,SAAA;EACA,OAAA,GAAU,MAAA;EAEV;EAAA,SAAA;AAAA;AAAA,UAGe,mBAAA,SAA4B,iBAAA;EAC3C,IAAA;EACA,OAAA;EACA,GAAA;EACA,GAAA;EACA,OAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,iBAAA;EAC5C,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,mBAAA,0DACP,iBAAA;EACR,IAAA;EACA,OAAA,EAAS,CAAA;EACT,OAAA,GAAU,CAAA;AAAA;AAAA,UAGK,iBAAA,sBAAuC,iBAAA;EACtD,IAAA;EACA,OAAA,GAAU,CAAA;EAZH;EAcP,MAAA,GAAS,OAAA,CAAQ,CAAA;EAXF;;;;;;;;;;;EAuBf,QAAA;AAAA;AAAA,UAGe,kBAAA,SAA2B,iBAAA;EAC1C,IAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,KAGU,aAAA,GACR,iBAAA,GACA,mBAAA,GACA,oBAAA,GACA,mBAAA,GACA,iBAAA,GACA,kBAAA;AAAA,KAgDQ,YAAA;AAAA,UAoCK,kBAAA,WACL,MAAA,SAAe,aAAA,IAAiB,MAAA,SAAe,aAAA;EArFvD;EAwFF,SAAA;EAxFoB;EA0FpB,KAAA,EAAO,YAAA;EA1Ce;EA4CtB,MAAA,EAAQ,CAAA;EA5Cc;EA8CtB,KAAA;EAVe;;;;;EAgBf,QAAA;EAf0C;;;;;EAqB1C,YAAA;AAAA;;;iBC1Kc,iBAAA,CACd,WAAA,EAAa,kBAAA,KACZ,KAAA;EAAQ,QAAA;EAAkB,OAAA;AAAA;;;;;;;;;;;;;ADmB7B;;;;;iBE6DgB,cAAA,CAAe,UAAA,EAAY,kBAAA,GAAqB,UAAA;AAAA,iBAChD,cAAA,CAAe,WAAA,EAAa,kBAAA,KAAuB,UAAA;AAAA,iBACnD,cAAA,CAAe,cAAA,QAAsB,kBAAA,KAAuB,UAAA"}
|
package/dist/admin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{validateLocale as e}from"@murumets-ee/core";function
|
|
1
|
+
import{validateLocale as e}from"@murumets-ee/core";import{ZodError as t}from"zod";function n(e){return e.map(e=>({resource:`settings:${e.namespace}`,actions:[`view`,`update`]}))}function r(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function i(e,t){return r({error:e},t)}function a(e){return{definition:e,clientPromise:null}}async function o(e,t){if(e.definition.scope===`user`){let{getApp:n}=await import(`@murumets-ee/core`),{createSettingsClient:r}=await import(`./client-factory-C44mk_oR.mjs`);return r(e.definition,{app:n(),scopeId:t})}return e.clientPromise||=(async()=>{let{getApp:t}=await import(`@murumets-ee/core`),{createSettingsClient:n}=await import(`./client-factory-C44mk_oR.mjs`);return n(e.definition,{app:t()})})(),e.clientPromise}function s(n){let s=typeof n==`function`?n:()=>Array.isArray(n)?n:[n],c=null,l=null,u=()=>{let e=s();if(c===null||e!==l){let t=new Map;for(let n of e)t.set(n.namespace,a(n));c=t,l=e}return c};async function d(t,n){let a=n.segments[0];if(!a)return i(`Missing settings namespace`,400);let s=u().get(a);if(!s)return i(`Unknown settings namespace: ${a}`,404);if(!n.checkPermission(`settings:${a}`,`view`))return n.audit?.({action:`settings.view.denied`,entityType:`settings`,userId:n.user.id,...n.user.name!==void 0&&{userName:n.user.name},metadata:{namespace:a,reason:`permission`}}),i(`Forbidden: cannot view settings:${a}`,403);let c=new URL(t.url),l;if(s.definition.scope===`user`&&(l=c.searchParams.get(`scopeId`)??n.user.id,l!==n.user.id))return n.audit?.({action:`settings.view.denied`,entityType:`settings`,userId:n.user.id,...n.user.name!==void 0&&{userName:n.user.name},metadata:{namespace:a,reason:`cross-user-scopeId`}}),i(`Forbidden: cannot read other users' preferences`,403);let d=await o(s,l),f=e(c.searchParams.get(`locale`));return r(await d.getAll(f?{locale:f}:void 0))}async function f(n,a){let s=a.segments[0];if(!s)return i(`Missing settings namespace`,400);let c=u().get(s);if(!c)return i(`Unknown settings namespace: ${s}`,404);if(!a.checkPermission(`settings:${s}`,`update`))return a.audit?.({action:`settings.update.denied`,entityType:`settings`,userId:a.user.id,...a.user.name!==void 0&&{userName:a.user.name},metadata:{namespace:s,reason:`permission`}}),i(`Forbidden: cannot update settings:${s}`,403);let{values:l,locale:d,scopeId:f}=await n.json(),p=e(d);if(!l||typeof l!=`object`||Array.isArray(l))return i(`Body must contain "values" object`,400);let m;if(c.definition.scope===`user`&&(m=f??a.user.id,m!==a.user.id))return a.audit?.({action:`settings.update.denied`,entityType:`settings`,userId:a.user.id,...a.user.name!==void 0&&{userName:a.user.name},metadata:{namespace:s,reason:`cross-user-scopeId`}}),i(`Forbidden: cannot update other users' preferences`,403);let h=await o(c,m);try{await h.setMany(l,p?{locale:p}:void 0)}catch(e){if(e instanceof t){let t={};for(let n of e.issues){let e=n.path.join(`.`)||`_`;t[e]=n.message}return a.audit?.({action:`settings.update.denied`,entityType:`settings`,userId:a.user.id,...a.user.name!==void 0&&{userName:a.user.name},metadata:{namespace:s,reason:`validation`,fields:Object.keys(t)}}),r({error:`Validation failed`,fieldErrors:t},400)}throw e}return a.audit?.({action:`settings.update`,entityType:`settings`,userId:a.user.id,...a.user.name!==void 0&&{userName:a.user.name},changes:{fields:l},metadata:{namespace:s,...p?{locale:p}:{},...m?{scopeId:m}:{}}}),r({success:!0})}return{prefix:`settings`,actions:[`view`,`update`],handlers:{GET:d,PATCH:f}}}export{n as settingsResources,s as settingsRoutes};
|
|
2
2
|
//# sourceMappingURL=admin.mjs.map
|
package/dist/admin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.mjs","names":[],"sources":["../src/admin/resources.ts","../src/admin/routes.ts"],"sourcesContent":["/**\n * Derive permission resource entries from settings definitions.\n *\n * Each definition produces one resource (`settings:<namespace>`) with\n * `['view', 'update']` actions. Spread the result into your\n * `pluginResources` array so the permission catalog includes them.\n *\n * @example\n * ```typescript\n * import { settingsResources } from '@murumets-ee/settings/admin'\n * import { siteSettings } from '@/settings/site'\n * import { ticketingSettings } from '@murumets-ee/ticketing'\n *\n * export const pluginResources = [\n * ...settingsResources([siteSettings, ticketingSettings]),\n * { resource: 'storage', actions: ['view', 'create', 'update', 'delete'] },\n * ]\n * ```\n */\n\nimport type { SettingsDefinition } from '../types.js'\n\nexport function settingsResources(\n definitions: SettingsDefinition[],\n): Array<{ resource: string; actions: readonly string[] }> {\n return definitions.map((def) => ({\n resource: `settings:${def.namespace}`,\n actions: ['view', 'update'] as const,\n }))\n}\n","/**\n * Settings admin routes for the centralized admin API handler.\n *\n * Provides get/update operations for typed settings, with per-namespace\n * permission checks. Accepts a single definition (backward-compat) or\n * an array of definitions for multi-namespace support.\n *\n * URL scheme:\n * - `GET /api/admin/settings/:namespace` — Get all settings (query: `?locale=`)\n * - `PATCH /api/admin/settings/:namespace` — Update settings (JSON body: `{ values, locale?, scopeId? }`)\n *\n * Each namespace is independently gated by `checkPermission('settings:<ns>', action)`.\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { settingsRoutes } from '@murumets-ee/settings/admin'\n * import { siteSettings } from '@/settings/site'\n * import { ticketingSettings, agentPreferences } from '@murumets-ee/ticketing'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [Article],\n * routes: [settingsRoutes([siteSettings, ticketingSettings, agentPreferences])],\n * })\n * ```\n */\n\nimport type { AdminRoute, AuditLogFn, AuthUser } from '@murumets-ee/core'\nimport { validateLocale } from '@murumets-ee/core'\nimport type { SettingsClient } from '../client.js'\nimport type { SettingConfig, SettingsDefinition } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Response helpers\n// ---------------------------------------------------------------------------\n\nfunction json(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction errorJson(message: string, status: number) {\n return json({ error: message }, status)\n}\n\n// ---------------------------------------------------------------------------\n// Per-namespace entry: definition + lazy client\n// ---------------------------------------------------------------------------\n\ninterface NamespaceEntry<S extends Record<string, SettingConfig> = Record<string, SettingConfig>> {\n definition: SettingsDefinition<S>\n clientPromise: Promise<SettingsClient<S>> | null\n}\n\nfunction createEntry<S extends Record<string, SettingConfig>>(\n definition: SettingsDefinition<S>,\n): NamespaceEntry<S> {\n return { definition, clientPromise: null }\n}\n\nasync function getClient<S extends Record<string, SettingConfig>>(\n entry: NamespaceEntry<S>,\n scopeId?: string,\n): Promise<SettingsClient<S>> {\n // User-scoped settings need a fresh client per scopeId — no caching\n if (entry.definition.scope === 'user') {\n const { getApp } = await import('@murumets-ee/core')\n const { createSettingsClient } = await import('../client-factory.js')\n return createSettingsClient(entry.definition, { app: getApp(), scopeId })\n }\n\n if (!entry.clientPromise) {\n entry.clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createSettingsClient } = await import('../client-factory.js')\n return createSettingsClient(entry.definition, { app: getApp() })\n })()\n }\n return entry.clientPromise\n}\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create admin API routes for settings management.\n *\n * Accepts a single definition (backward-compatible) or an array for\n * multi-namespace support. Each namespace gets its own permission\n * resource (`settings:<namespace>`).\n */\nexport function settingsRoutes(definition: SettingsDefinition): AdminRoute\nexport function settingsRoutes(definitions: SettingsDefinition[]): AdminRoute\nexport function settingsRoutes(\n input: SettingsDefinition | SettingsDefinition[],\n): AdminRoute {\n const definitions = Array.isArray(input) ? input : [input]\n\n // Build lookup map: namespace → entry\n const nsMap = new Map<string, NamespaceEntry>()\n for (const def of definitions) {\n nsMap.set(def.namespace, createEntry(def))\n }\n\n // ---------- GET /api/admin/settings/:namespace ----------\n async function handleGet(\n req: Request,\n ctx: {\n segments: string[]\n user: AuthUser\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n ): Promise<Response> {\n const namespace = ctx.segments[0]\n if (!namespace) return errorJson('Missing settings namespace', 400)\n\n const entry = nsMap.get(namespace)\n if (!entry) return errorJson(`Unknown settings namespace: ${namespace}`, 404)\n\n // Per-namespace permission check\n if (!ctx.checkPermission(`settings:${namespace}`, 'view')) {\n return errorJson(`Forbidden: cannot view settings:${namespace}`, 403)\n }\n\n // User-scoped: only allow reading own preferences\n const url = new URL(req.url)\n let scopeId: string | undefined\n if (entry.definition.scope === 'user') {\n scopeId = url.searchParams.get('scopeId') ?? ctx.user.id\n if (scopeId !== ctx.user.id) {\n return errorJson('Forbidden: cannot read other users\\' preferences', 403)\n }\n }\n\n const client = await getClient(entry, scopeId)\n const locale = validateLocale(url.searchParams.get('locale'))\n const values = await client.getAll(locale ? { locale } : undefined)\n return json(values)\n }\n\n // ---------- PATCH /api/admin/settings/:namespace ----------\n async function handlePatch(\n req: Request,\n ctx: {\n segments: string[]\n user: AuthUser\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n ): Promise<Response> {\n const namespace = ctx.segments[0]\n if (!namespace) return errorJson('Missing settings namespace', 400)\n\n const entry = nsMap.get(namespace)\n if (!entry) return errorJson(`Unknown settings namespace: ${namespace}`, 404)\n\n // Per-namespace permission check\n if (!ctx.checkPermission(`settings:${namespace}`, 'update')) {\n return errorJson(`Forbidden: cannot update settings:${namespace}`, 403)\n }\n\n const body = await req.json()\n const { values, locale: rawLocale, scopeId } = body as {\n values: Record<string, unknown>\n locale?: string\n scopeId?: string\n }\n const locale = validateLocale(rawLocale)\n\n if (!values || typeof values !== 'object') {\n return errorJson('Body must contain \"values\" object', 400)\n }\n\n // User-scoped: enforce user isolation\n let resolvedScopeId: string | undefined\n if (entry.definition.scope === 'user') {\n resolvedScopeId = scopeId ?? ctx.user.id\n if (resolvedScopeId !== ctx.user.id) {\n return errorJson('Forbidden: cannot update other users\\' preferences', 403)\n }\n }\n\n const client = await getClient(entry, resolvedScopeId)\n await client.setMany(\n values as Parameters<typeof client.setMany>[0],\n locale ? { locale } : undefined,\n )\n\n ctx.audit?.({\n action: 'settings.update',\n entityType: 'settings',\n userId: ctx.user.id,\n userName: ctx.user.name,\n changes: { fields: values },\n metadata: {\n namespace,\n ...(locale ? { locale } : {}),\n ...(resolvedScopeId ? { scopeId: resolvedScopeId } : {}),\n },\n })\n\n return json({ success: true })\n }\n\n return {\n prefix: 'settings',\n // No `resource` — permission checks happen per-namespace inside handlers\n actions: ['view', 'update'],\n handlers: {\n GET: handleGet,\n PATCH: handlePatch,\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"admin.mjs","names":[],"sources":["../src/admin/resources.ts","../src/admin/routes.ts"],"sourcesContent":["/**\n * Derive permission resource entries from settings definitions.\n *\n * Each definition produces one resource (`settings:<namespace>`) with\n * `['view', 'update']` actions. Spread the result into your\n * `pluginResources` array so the permission catalog includes them.\n *\n * @example\n * ```typescript\n * import { settingsResources } from '@murumets-ee/settings/admin'\n * import { siteSettings } from '@/settings/site'\n * import { ticketingSettings } from '@murumets-ee/ticketing'\n *\n * export const pluginResources = [\n * ...settingsResources([siteSettings, ticketingSettings]),\n * { resource: 'storage', actions: ['view', 'create', 'update', 'delete'] },\n * ]\n * ```\n */\n\nimport type { SettingsDefinition } from '../types.js'\n\nexport function settingsResources(\n definitions: SettingsDefinition[],\n): Array<{ resource: string; actions: readonly string[] }> {\n return definitions.map((def) => ({\n resource: `settings:${def.namespace}`,\n actions: ['view', 'update'] as const,\n }))\n}\n","/**\n * Settings admin routes for the centralized admin API handler.\n *\n * Provides get/update operations for typed settings, with per-namespace\n * permission checks. Accepts a single definition (backward-compat) or\n * an array of definitions for multi-namespace support.\n *\n * URL scheme:\n * - `GET /api/admin/settings/:namespace` — Get all settings (query: `?locale=`)\n * - `PATCH /api/admin/settings/:namespace` — Update settings (JSON body: `{ values, locale?, scopeId? }`)\n *\n * Each namespace is independently gated by `checkPermission('settings:<ns>', action)`.\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { settingsRoutes } from '@murumets-ee/settings/admin'\n * import { siteSettings } from '@/settings/site'\n * import { ticketingSettings, agentPreferences } from '@murumets-ee/ticketing'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [Article],\n * routes: [settingsRoutes([siteSettings, ticketingSettings, agentPreferences])],\n * })\n * ```\n */\n\nimport type { AdminRoute, AuditLogFn, AuthUser } from '@murumets-ee/core'\nimport { validateLocale } from '@murumets-ee/core'\nimport { ZodError } from 'zod'\nimport type { SettingsClient } from '../client.js'\nimport type { SettingConfig, SettingsDefinition } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Response helpers\n// ---------------------------------------------------------------------------\n\nfunction json(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction errorJson(message: string, status: number) {\n return json({ error: message }, status)\n}\n\n// ---------------------------------------------------------------------------\n// Per-namespace entry: definition + lazy client\n// ---------------------------------------------------------------------------\n\ninterface NamespaceEntry<S extends Record<string, SettingConfig> = Record<string, SettingConfig>> {\n definition: SettingsDefinition<S>\n clientPromise: Promise<SettingsClient<S>> | null\n}\n\nfunction createEntry<S extends Record<string, SettingConfig>>(\n definition: SettingsDefinition<S>,\n): NamespaceEntry<S> {\n return { definition, clientPromise: null }\n}\n\nasync function getClient<S extends Record<string, SettingConfig>>(\n entry: NamespaceEntry<S>,\n scopeId?: string,\n): Promise<SettingsClient<S>> {\n // User-scoped settings need a fresh client per scopeId — no caching\n if (entry.definition.scope === 'user') {\n const { getApp } = await import('@murumets-ee/core')\n const { createSettingsClient } = await import('../client-factory.js')\n return createSettingsClient(entry.definition, { app: getApp(), scopeId })\n }\n\n if (!entry.clientPromise) {\n entry.clientPromise = (async () => {\n const { getApp } = await import('@murumets-ee/core')\n const { createSettingsClient } = await import('../client-factory.js')\n return createSettingsClient(entry.definition, { app: getApp() })\n })()\n }\n return entry.clientPromise\n}\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create admin API routes for settings management.\n *\n * Accepts a single definition, an array, or a thunk returning the\n * current definitions list. The thunk form lets the settings plugin\n * aggregate contributions from other plugins at init time — its init\n * hook walks `app.plugins.all()`, pulls each plugin's `shared.settings`,\n * and pushes them into a mutable list, which the route handler reads\n * lazily on first request (after init has run).\n *\n * Each namespace gets its own permission resource (`settings:<namespace>`).\n * The merge engine in `@murumets-ee/core` auto-derives that resource and\n * the sidebar entry from `Plugin.shared.settings`, so plugin authors only\n * need to declare the namespace once.\n */\nexport function settingsRoutes(definition: SettingsDefinition): AdminRoute\nexport function settingsRoutes(definitions: SettingsDefinition[]): AdminRoute\nexport function settingsRoutes(getDefinitions: () => SettingsDefinition[]): AdminRoute\nexport function settingsRoutes(\n input: SettingsDefinition | SettingsDefinition[] | (() => SettingsDefinition[]),\n): AdminRoute {\n const getDefs: () => SettingsDefinition[] =\n typeof input === 'function' ? input : () => (Array.isArray(input) ? input : [input])\n\n // Lazy nsMap — built on first request so plugin-init contributions are\n // visible. Rebuilt automatically when the thunk returns a new array\n // reference (e.g. across tests with separate registries).\n let cachedNsMap: Map<string, NamespaceEntry> | null = null\n let lastDefs: SettingsDefinition[] | null = null\n const getNsMap = (): Map<string, NamespaceEntry> => {\n const defs = getDefs()\n if (cachedNsMap === null || defs !== lastDefs) {\n const next = new Map<string, NamespaceEntry>()\n for (const def of defs) {\n next.set(def.namespace, createEntry(def))\n }\n cachedNsMap = next\n lastDefs = defs\n }\n return cachedNsMap\n }\n\n // ---------- GET /api/admin/settings/:namespace ----------\n async function handleGet(\n req: Request,\n ctx: {\n segments: string[]\n user: AuthUser\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n ): Promise<Response> {\n const namespace = ctx.segments[0]\n if (!namespace) return errorJson('Missing settings namespace', 400)\n\n const entry = getNsMap().get(namespace)\n if (!entry) return errorJson(`Unknown settings namespace: ${namespace}`, 404)\n\n // Per-namespace permission check\n if (!ctx.checkPermission(`settings:${namespace}`, 'view')) {\n ctx.audit?.({\n action: 'settings.view.denied',\n entityType: 'settings',\n userId: ctx.user.id,\n ...(ctx.user.name !== undefined && { userName: ctx.user.name }),\n metadata: { namespace, reason: 'permission' },\n })\n return errorJson(`Forbidden: cannot view settings:${namespace}`, 403)\n }\n\n // User-scoped: only allow reading own preferences\n const url = new URL(req.url)\n let scopeId: string | undefined\n if (entry.definition.scope === 'user') {\n scopeId = url.searchParams.get('scopeId') ?? ctx.user.id\n if (scopeId !== ctx.user.id) {\n ctx.audit?.({\n action: 'settings.view.denied',\n entityType: 'settings',\n userId: ctx.user.id,\n ...(ctx.user.name !== undefined && { userName: ctx.user.name }),\n metadata: { namespace, reason: 'cross-user-scopeId' },\n })\n return errorJson(\"Forbidden: cannot read other users' preferences\", 403)\n }\n }\n\n const client = await getClient(entry, scopeId)\n const locale = validateLocale(url.searchParams.get('locale'))\n const values = await client.getAll(locale ? { locale } : undefined)\n return json(values)\n }\n\n // ---------- PATCH /api/admin/settings/:namespace ----------\n async function handlePatch(\n req: Request,\n ctx: {\n segments: string[]\n user: AuthUser\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n ): Promise<Response> {\n const namespace = ctx.segments[0]\n if (!namespace) return errorJson('Missing settings namespace', 400)\n\n const entry = getNsMap().get(namespace)\n if (!entry) return errorJson(`Unknown settings namespace: ${namespace}`, 404)\n\n // Per-namespace permission check\n if (!ctx.checkPermission(`settings:${namespace}`, 'update')) {\n ctx.audit?.({\n action: 'settings.update.denied',\n entityType: 'settings',\n userId: ctx.user.id,\n ...(ctx.user.name !== undefined && { userName: ctx.user.name }),\n metadata: { namespace, reason: 'permission' },\n })\n return errorJson(`Forbidden: cannot update settings:${namespace}`, 403)\n }\n\n const body = (await req.json()) as {\n values: Record<string, unknown>\n locale?: string\n scopeId?: string\n }\n const { values, locale: rawLocale, scopeId } = body\n const locale = validateLocale(rawLocale)\n\n if (!values || typeof values !== 'object' || Array.isArray(values)) {\n return errorJson('Body must contain \"values\" object', 400)\n }\n\n // User-scoped: enforce user isolation\n let resolvedScopeId: string | undefined\n if (entry.definition.scope === 'user') {\n resolvedScopeId = scopeId ?? ctx.user.id\n if (resolvedScopeId !== ctx.user.id) {\n ctx.audit?.({\n action: 'settings.update.denied',\n entityType: 'settings',\n userId: ctx.user.id,\n ...(ctx.user.name !== undefined && { userName: ctx.user.name }),\n metadata: { namespace, reason: 'cross-user-scopeId' },\n })\n return errorJson(\"Forbidden: cannot update other users' preferences\", 403)\n }\n }\n\n const client = await getClient(entry, resolvedScopeId)\n try {\n await client.setMany(\n values as Parameters<typeof client.setMany>[0],\n locale ? { locale } : undefined,\n )\n } catch (err) {\n if (err instanceof ZodError) {\n // Per-field validation failures: surface them with enough detail for\n // the form to highlight the offending field. Audited as a denied\n // update so spammy invalid payloads stay traceable.\n const fieldErrors: Record<string, string> = {}\n for (const issue of err.issues) {\n const path = issue.path.join('.') || '_'\n fieldErrors[path] = issue.message\n }\n ctx.audit?.({\n action: 'settings.update.denied',\n entityType: 'settings',\n userId: ctx.user.id,\n ...(ctx.user.name !== undefined && { userName: ctx.user.name }),\n metadata: { namespace, reason: 'validation', fields: Object.keys(fieldErrors) },\n })\n return json({ error: 'Validation failed', fieldErrors }, 400)\n }\n throw err\n }\n\n ctx.audit?.({\n action: 'settings.update',\n entityType: 'settings',\n userId: ctx.user.id,\n ...(ctx.user.name !== undefined && { userName: ctx.user.name }),\n changes: { fields: values },\n metadata: {\n namespace,\n ...(locale ? { locale } : {}),\n ...(resolvedScopeId ? { scopeId: resolvedScopeId } : {}),\n },\n })\n\n return json({ success: true })\n }\n\n return {\n prefix: 'settings',\n // No `resource` — permission checks happen per-namespace inside handlers\n actions: ['view', 'update'],\n handlers: {\n GET: handleGet,\n PATCH: handlePatch,\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{n as e}from"./define-settings-DHaHn6XA.mjs";import{toolkitSettingsTable as t}from"./schema.mjs";import{ZodError as n,z as r}from"zod";import{getApp as i}from"@murumets-ee/core";function a(e){switch(e.type){case`text`:{let t=r.string();return t=t.max(e.maxLength??65536),e.minLength&&(t=t.min(e.minLength)),e.pattern&&(t=t.regex(e.pattern)),t}case`number`:{let t=r.number();return e.integer&&(t=t.int()),e.min!==void 0&&(t=t.min(e.min)),e.max!==void 0&&(t=t.max(e.max)),t}case`boolean`:return r.boolean();case`select`:return r.enum(e.options);case`json`:return e.schema??r.unknown();case`media`:return r.string().uuid();default:return r.unknown()}}function o(e){let t={};for(let[n,r]of Object.entries(e.schema)){let e=a(r);`default`in r&&r.default!==void 0||(e=e.nullable()),t[n]=e}return t}var s=class{definition;table;logger;scope;scopeId;validators;constructor(e,n){if(typeof window<`u`)throw Error(`SettingsClient cannot be used in browser code.`);if(this.definition=e,this.table=t.makeClient(n.db),this.logger=n.logger,this.scope=n.scope??e.scope,this.scopeId=n.scopeId??`__global__`,(this.scope===`team`||this.scope===`user`)&&!n.scopeId)throw Error(`scopeId is required for ${this.scope}-scoped settings (namespace: ${e.namespace})`);this.validators=o(e)}async get(t,n){let r=this.definition.schema[t],i=r?.translatable&&n?.locale?n.locale:null;this.logger?.debug({namespace:this.definition.namespace,key:t,locale:i},`Getting setting`);let a;if(i){let n=await this.findRows({...this.baseWhere(),key:t,$or:[{locale:i},{locale:e}]},2),r=n.find(e=>e.locale===i),o=n.find(t=>t.locale===e),s=r??o;s&&s.value!==void 0&&s.value!==null&&(a=s.value)}else{let n=(await this.findRows({...this.baseWhere(),key:t,locale:e},1))[0];n&&n.value!==void 0&&n.value!==null&&(a=n.value)}return a===void 0&&r&&`default`in r&&r.default!==void 0&&(a=r.default),a??null}async getAll(t){let n=t?.locale??null;this.logger?.debug({namespace:this.definition.namespace,locale:n},`Getting all settings`);let r=n?await this.findRows({...this.baseWhere(),$or:[{locale:e},{locale:n}]},1e3):await this.findRows({...this.baseWhere(),locale:e},1e3),i=new Map;for(let e of r){let t=i.get(e.key)??{};e.locale===`_default`?t.default=e.value:t.locale=e.value,i.set(e.key,t)}let a={};for(let[e,t]of Object.entries(this.definition.schema)){let r=i.get(e),o;t.translatable&&n&&r?.locale!==void 0&&r?.locale!==null?o=r.locale:r?.default!==void 0&&r?.default!==null&&(o=r.default),o===void 0?`default`in t&&t.default!==void 0?a[e]=t.default:a[e]=null:a[e]=o}return a}async set(e,t,r){let i=this.resolveLocale(e,r);this.logger?.info({namespace:this.definition.namespace,key:e,locale:i},`Setting value`),this.assertKnownKey(e);let a=this.validators[e];if(a)try{a.parse(t)}catch(t){throw t instanceof n?new n(t.issues.map(t=>({...t,path:[e,...t.path]}))):t}await this.upsertRow(e,t,i,this.table)}async setMany(e,t){this.logger?.info({namespace:this.definition.namespace,keys:Object.keys(e),locale:t?.locale},`Setting multiple values`);for(let[t,r]of Object.entries(e)){if(this.assertKnownKey(t),r===void 0)continue;let e=this.validators[t];if(e)try{e.parse(r)}catch(e){throw e instanceof n?new n(e.issues.map(e=>({...e,path:[t,...e.path]}))):e}}await this.table.transaction(async n=>{for(let[r,i]of Object.entries(e)){if(i===void 0)continue;let e=this.resolveLocale(r,t);await this.upsertRow(r,i,e,n)}})}async delete(e,t){let n=this.resolveLocale(e,t);this.logger?.info({namespace:this.definition.namespace,key:e,locale:n},`Deleting setting`),await this.table.deleteMany({...this.baseWhere(),key:e,locale:n})}async has(e,t){let n=this.resolveLocale(e,t);return await this.table.exists({...this.baseWhere(),key:e,locale:n})}assertKnownKey(e){if(!(e in this.definition.schema))throw this.logger?.warn({namespace:this.definition.namespace,key:e},`Rejected unknown setting key`),Error(`Unknown setting key`)}resolveLocale(t,n){return this.definition.schema[t]?.translatable&&n?.locale?n.locale:e}baseWhere(){return{namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId}}async findRows(e,t){return await this.table.findMany({where:e,limit:t})}async upsertRow(e,t,n,r){await r.upsert({namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId,key:e,locale:n,value:t,updatedAt:new Date},{target:[`namespace`,`scope`,`scopeId`,`key`,`locale`],set:{value:t,updatedAt:new Date}})}};function c(e,t){let n=t?.app??i();return new s(e,{db:n.db.readWrite,logger:n.logger.child({settings:e.namespace}),scope:t?.scope,scopeId:t?.scopeId})}export{s as n,c as t};
|
|
2
|
+
//# sourceMappingURL=client-factory-B8jGDCJn.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-factory-B8jGDCJn.mjs","names":[],"sources":["../src/validation.ts","../src/client.ts","../src/client-factory.ts"],"sourcesContent":["/**\n * Generate Zod validation schemas from setting config definitions.\n * Validates values before writing to the database.\n */\n\nimport { z } from 'zod'\nimport type { SettingConfig, SettingsDefinition } from './types.js'\n\n/**\n * Default cap for text settings without an explicit `maxLength`.\n * 64 KiB — defense-in-depth against an admin uploading multi-MB blobs into\n * a `varchar`/`text`-shaped setting.\n */\nconst DEFAULT_TEXT_MAX_LENGTH = 65_536\n\n/**\n * Convert a single setting config to a Zod schema.\n */\nexport function settingToZod(config: SettingConfig): z.ZodType {\n switch (config.type) {\n case 'text': {\n let schema: z.ZodString = z.string()\n schema = schema.max(config.maxLength ?? DEFAULT_TEXT_MAX_LENGTH)\n if (config.minLength) schema = schema.min(config.minLength)\n if (config.pattern) schema = schema.regex(config.pattern)\n return schema\n }\n\n case 'number': {\n let schema: z.ZodNumber = z.number()\n if (config.integer) schema = schema.int()\n if (config.min !== undefined) schema = schema.min(config.min)\n if (config.max !== undefined) schema = schema.max(config.max)\n return schema\n }\n\n case 'boolean':\n return z.boolean()\n\n case 'select':\n return z.enum(config.options as [string, ...string[]])\n\n case 'json':\n return config.schema ?? z.unknown()\n\n case 'media':\n return z.string().uuid()\n\n default:\n return z.unknown()\n }\n}\n\n/**\n * Generate a validation map for all settings in a definition.\n * Returns a Record<key, ZodType> for validating individual set() calls.\n *\n * Settings without a default are nullable (matching InferSettingValue),\n * so their validators accept null.\n */\nexport function generateSettingValidators(\n definition: SettingsDefinition,\n): Record<string, z.ZodType> {\n const validators: Record<string, z.ZodType> = {}\n for (const [key, config] of Object.entries(definition.schema)) {\n let schema = settingToZod(config)\n const hasDefault = 'default' in config && config.default !== undefined\n if (!hasDefault) {\n schema = schema.nullable()\n }\n validators[key] = schema\n }\n return validators\n}\n","/**\n * SettingsClient — typed CRUD for key-value settings.\n *\n * Server-only. Uses read-write DB connection.\n * Validates values against the setting schema on set().\n *\n * Supports per-locale values for settings marked `translatable: true`.\n * Non-translatable settings always use the `__default__` locale.\n *\n * All persistence goes through the `toolkit_settings` TableClient\n * (`defineTable`) — no direct Drizzle usage from this module.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n *\n * // Default locale\n * const name = await settings.get('siteName') // string (has default)\n *\n * // Locale-specific (only for translatable settings)\n * const nameEt = await settings.get('siteName', { locale: 'et' })\n *\n * await settings.set('siteName', 'Mänguväljak', { locale: 'et' })\n * ```\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { WhereClause } from '@murumets-ee/db'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { type ZodType, ZodError } from 'zod'\nimport { toolkitSettingsTable } from './schema.js'\nimport type {\n InferSettingsMap,\n InferSettingValue,\n SettingConfig,\n SettingScope,\n SettingsDefinition,\n} from './types.js'\nimport { DEFAULT_LOCALE, GLOBAL_SCOPE_ID } from './types.js'\nimport { generateSettingValidators } from './validation.js'\n\nexport interface SettingsClientConfig {\n /** Database client (read-write) */\n db: PostgresJsDatabase\n /** Logger instance */\n logger?: Logger | undefined\n /** Override scope (defaults to definition's scope) */\n scope?: SettingScope | undefined\n /** Scope ID (required for team/user scope, defaults to '__global__' for global) */\n scopeId?: string | undefined\n}\n\nexport interface LocaleOption {\n /** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */\n locale?: string\n}\n\n/** Row shape returned by the settings table (for narrow typing of fetched rows). */\ninterface SettingsRow {\n key: string\n locale: string\n value: unknown\n}\n\ntype TableClient = ReturnType<typeof toolkitSettingsTable.makeClient>\ntype SettingsTableColumns = (typeof toolkitSettingsTable)['schema']['columns']\ntype FindWhere = WhereClause<SettingsTableColumns>\n\nexport class SettingsClient<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n private definition: SettingsDefinition<S>\n private table: TableClient\n private logger?: Logger | undefined\n private scope: SettingScope\n private scopeId: string\n private validators: Record<string, ZodType>\n\n constructor(definition: SettingsDefinition<S>, config: SettingsClientConfig) {\n if (typeof window !== 'undefined') {\n throw new Error('SettingsClient cannot be used in browser code.')\n }\n\n this.definition = definition\n this.table = toolkitSettingsTable.makeClient(config.db)\n this.logger = config.logger\n this.scope = config.scope ?? definition.scope\n this.scopeId = config.scopeId ?? GLOBAL_SCOPE_ID\n\n if ((this.scope === 'team' || this.scope === 'user') && !config.scopeId) {\n throw new Error(\n `scopeId is required for ${this.scope}-scoped settings (namespace: ${definition.namespace})`,\n )\n }\n\n this.validators = generateSettingValidators(definition)\n }\n\n /**\n * Get a single setting value.\n *\n * For translatable settings with a locale, tries locale-specific value first,\n * then falls back to the default value, then the schema default, then null.\n */\n async get<K extends string & keyof S>(\n key: K,\n options?: LocaleOption,\n ): Promise<InferSettingValue<S[K]>> {\n const config = this.definition.schema[key]\n const locale = config?.translatable && options?.locale ? options.locale : null\n\n this.logger?.debug({ namespace: this.definition.namespace, key, locale }, 'Getting setting')\n\n let resolved: unknown\n\n if (locale) {\n const rows = await this.findRows(\n {\n ...this.baseWhere(),\n key,\n $or: [{ locale }, { locale: DEFAULT_LOCALE }],\n },\n 2,\n )\n\n const localeRow = rows.find((r) => r.locale === locale)\n const defaultRow = rows.find((r) => r.locale === DEFAULT_LOCALE)\n const row = localeRow ?? defaultRow\n\n if (row && row.value !== undefined && row.value !== null) {\n resolved = row.value\n }\n } else {\n const rows = await this.findRows({ ...this.baseWhere(), key, locale: DEFAULT_LOCALE }, 1)\n\n const first = rows[0]\n if (first && first.value !== undefined && first.value !== null) {\n resolved = first.value\n }\n }\n\n if (resolved === undefined && config && 'default' in config && config.default !== undefined) {\n resolved = config.default\n }\n\n return (resolved ?? null) as InferSettingValue<S[K]>\n }\n\n /**\n * Get all settings for this namespace/scope as a typed object.\n * Missing values are filled from schema defaults.\n *\n * When locale is specified, translatable settings prefer the locale-specific\n * value over the default value.\n */\n async getAll(options?: LocaleOption): Promise<InferSettingsMap<S>> {\n const locale = options?.locale ?? null\n\n this.logger?.debug({ namespace: this.definition.namespace, locale }, 'Getting all settings')\n\n const rows = locale\n ? await this.findRows(\n {\n ...this.baseWhere(),\n $or: [{ locale: DEFAULT_LOCALE }, { locale }],\n },\n 1000,\n )\n : await this.findRows({ ...this.baseWhere(), locale: DEFAULT_LOCALE }, 1000)\n\n // Build lookup: key → { default: value, locale: value }\n const stored = new Map<string, { default?: unknown; locale?: unknown }>()\n for (const row of rows) {\n const entry = stored.get(row.key) ?? {}\n if (row.locale === DEFAULT_LOCALE) {\n entry.default = row.value\n } else {\n entry.locale = row.value\n }\n stored.set(row.key, entry)\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, config] of Object.entries(this.definition.schema)) {\n const entry = stored.get(key)\n\n // For translatable settings with locale, prefer locale-specific value\n let value: unknown\n if (config.translatable && locale && entry?.locale !== undefined && entry?.locale !== null) {\n value = entry.locale\n } else if (entry?.default !== undefined && entry?.default !== null) {\n value = entry.default\n }\n\n if (value !== undefined) {\n result[key] = value\n } else if ('default' in config && config.default !== undefined) {\n result[key] = config.default\n } else {\n result[key] = null\n }\n }\n\n return result as InferSettingsMap<S>\n }\n\n /**\n * Set a single setting value.\n * Validates against the schema before writing.\n *\n * Pass `{ locale }` to write a locale-specific value (only for translatable settings).\n */\n async set<K extends string & keyof S>(\n key: K,\n value: InferSettingValue<S[K]>,\n options?: LocaleOption,\n ): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Setting value')\n\n this.assertKnownKey(key)\n const validator = this.validators[key]\n if (validator) {\n try {\n validator.parse(value)\n } catch (err) {\n if (err instanceof ZodError) {\n throw new ZodError(\n err.issues.map((issue) => ({ ...issue, path: [key, ...issue.path] })),\n )\n }\n throw err\n }\n }\n\n await this.upsertRow(key, value, locale, this.table)\n }\n\n /**\n * Set multiple settings at once (validated individually).\n * Writes in a transaction — all or nothing.\n *\n * Pass `{ locale }` to write locale-specific values for translatable settings.\n * Non-translatable settings in the values will always write to the default locale.\n */\n async setMany(values: Partial<InferSettingsMap<S>>, options?: LocaleOption): Promise<void> {\n this.logger?.info(\n { namespace: this.definition.namespace, keys: Object.keys(values), locale: options?.locale },\n 'Setting multiple values',\n )\n\n // Validate all first (fail fast). Zod's `issue.path` is RELATIVE to\n // the validator we invoke — for a top-level field with a\n // `setting.text({ minLength: 1 })` the path is `[]`; for a nested\n // failure inside a json schema, e.g. `imageStyles[thumb].width`,\n // it's `['thumb', 'width']`. To make the errors useful upstream\n // (the PATCH route turns `path` into a fieldErrors map keyed by the\n // setting name), we prefix each issue with the offending setting key\n // before re-throwing.\n for (const [key, value] of Object.entries(values)) {\n this.assertKnownKey(key)\n if (value === undefined) continue\n const validator = this.validators[key]\n if (!validator) continue\n try {\n validator.parse(value)\n } catch (err) {\n if (err instanceof ZodError) {\n throw new ZodError(\n err.issues.map((issue) => ({ ...issue, path: [key, ...issue.path] })),\n )\n }\n throw err\n }\n }\n\n await this.table.transaction(async (tx) => {\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n const locale = this.resolveLocale(key, options)\n await this.upsertRow(key, value, locale, tx)\n }\n })\n }\n\n /**\n * Delete a setting (resets to default on next get).\n * Pass `{ locale }` to delete only the locale-specific value.\n */\n async delete<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Deleting setting')\n\n await this.table.deleteMany({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n /**\n * Check if a setting has a stored value (not relying on default).\n */\n async has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean> {\n const locale = this.resolveLocale(key, options)\n\n return await this.table.exists({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n // ---------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------\n\n /**\n * Reject keys not declared in this namespace's schema.\n * Generic message — does NOT echo the key or namespace back to callers,\n * to avoid leaking schema structure on the admin route's error path.\n */\n private assertKnownKey(key: string): void {\n if (!(key in this.definition.schema)) {\n this.logger?.warn(\n { namespace: this.definition.namespace, key },\n 'Rejected unknown setting key',\n )\n throw new Error('Unknown setting key')\n }\n }\n\n /**\n * Resolve which locale to use for a given key.\n * Non-translatable settings always use DEFAULT_LOCALE.\n * Translatable settings use the requested locale or DEFAULT_LOCALE.\n */\n private resolveLocale(key: string, options?: LocaleOption): string {\n const config = this.definition.schema[key]\n if (config?.translatable && options?.locale) {\n return options.locale\n }\n return DEFAULT_LOCALE\n }\n\n /** Base where conditions: namespace + scope + scopeId. */\n private baseWhere() {\n return {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n }\n }\n\n /**\n * Typed wrapper around `table.findMany` — concentrates the cast that bridges\n * the wide TableClient row type to our narrow `SettingsRow` view.\n */\n private async findRows(where: FindWhere, limit: number): Promise<SettingsRow[]> {\n return (await this.table.findMany({ where, limit })) as unknown as SettingsRow[]\n }\n\n /**\n * Upsert a single setting row via the TableClient.\n * Accepts either the top-level table client or a transactional one —\n * same shape, used inside `setMany`'s transaction.\n */\n private async upsertRow(key: string, value: unknown, locale: string, tableClient: TableClient) {\n await tableClient.upsert(\n {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n key,\n locale,\n value,\n updatedAt: new Date(),\n },\n {\n target: ['namespace', 'scope', 'scopeId', 'key', 'locale'],\n set: {\n value,\n updatedAt: new Date(),\n },\n },\n )\n }\n}\n","/**\n * Factory function for creating typed SettingsClient instances.\n *\n * Follows the createAdminClient(entity, app?) pattern from @murumets-ee/core.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { getApp } from '@murumets-ee/core'\nimport { SettingsClient } from './client.js'\nimport type { SettingConfig, SettingScope, SettingsDefinition } from './types.js'\n\nexport interface CreateSettingsClientOptions {\n /** Override the toolkit app (defaults to getApp()) */\n app?: ToolkitApp | undefined\n /** Override scope from definition */\n scope?: SettingScope | undefined\n /** Scope ID for team/user scoped settings */\n scopeId?: string | undefined\n}\n\n/**\n * Create a typed SettingsClient.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n * const name = await settings.get('siteName') // string\n * ```\n */\nexport function createSettingsClient<S extends Record<string, SettingConfig>>(\n definition: SettingsDefinition<S>,\n options?: CreateSettingsClientOptions,\n): SettingsClient<S> {\n const app = options?.app ?? getApp()\n\n return new SettingsClient(definition, {\n db: app.db.readWrite,\n logger: app.logger.child({ settings: definition.namespace }),\n scope: options?.scope,\n scopeId: options?.scopeId,\n })\n}\n"],"mappings":"wLAkBA,SAAgB,EAAa,EAAkC,CAC7D,OAAQ,EAAO,KAAf,CACE,IAAK,OAAQ,CACX,IAAI,EAAsB,EAAE,QAAQ,CAIpC,MAHA,GAAS,EAAO,IAAI,EAAO,WAAa,MAAwB,CAC5D,EAAO,YAAW,EAAS,EAAO,IAAI,EAAO,UAAU,EACvD,EAAO,UAAS,EAAS,EAAO,MAAM,EAAO,QAAQ,EAClD,EAGT,IAAK,SAAU,CACb,IAAI,EAAsB,EAAE,QAAQ,CAIpC,OAHI,EAAO,UAAS,EAAS,EAAO,KAAK,EACrC,EAAO,MAAQ,IAAA,KAAW,EAAS,EAAO,IAAI,EAAO,IAAI,EACzD,EAAO,MAAQ,IAAA,KAAW,EAAS,EAAO,IAAI,EAAO,IAAI,EACtD,EAGT,IAAK,UACH,OAAO,EAAE,SAAS,CAEpB,IAAK,SACH,OAAO,EAAE,KAAK,EAAO,QAAiC,CAExD,IAAK,OACH,OAAO,EAAO,QAAU,EAAE,SAAS,CAErC,IAAK,QACH,OAAO,EAAE,QAAQ,CAAC,MAAM,CAE1B,QACE,OAAO,EAAE,SAAS,EAWxB,SAAgB,EACd,EAC2B,CAC3B,IAAM,EAAwC,EAAE,CAChD,IAAK,GAAM,CAAC,EAAK,KAAW,OAAO,QAAQ,EAAW,OAAO,CAAE,CAC7D,IAAI,EAAS,EAAa,EAAO,CACd,YAAa,GAAU,EAAO,UAAY,IAAA,KAE3D,EAAS,EAAO,UAAU,EAE5B,EAAW,GAAO,EAEpB,OAAO,ECDT,IAAa,EAAb,KAEE,CACA,WACA,MACA,OACA,MACA,QACA,WAEA,YAAY,EAAmC,EAA8B,CAC3E,GAAI,OAAO,OAAW,IACpB,MAAU,MAAM,iDAAiD,CASnE,GANA,KAAK,WAAa,EAClB,KAAK,MAAQ,EAAqB,WAAW,EAAO,GAAG,CACvD,KAAK,OAAS,EAAO,OACrB,KAAK,MAAQ,EAAO,OAAS,EAAW,MACxC,KAAK,QAAU,EAAO,SAAA,cAEjB,KAAK,QAAU,QAAU,KAAK,QAAU,SAAW,CAAC,EAAO,QAC9D,MAAU,MACR,2BAA2B,KAAK,MAAM,+BAA+B,EAAW,UAAU,GAC3F,CAGH,KAAK,WAAa,EAA0B,EAAW,CASzD,MAAM,IACJ,EACA,EACkC,CAClC,IAAM,EAAS,KAAK,WAAW,OAAO,GAChC,EAAS,GAAQ,cAAgB,GAAS,OAAS,EAAQ,OAAS,KAE1E,KAAK,QAAQ,MAAM,CAAE,UAAW,KAAK,WAAW,UAAW,MAAK,SAAQ,CAAE,kBAAkB,CAE5F,IAAI,EAEJ,GAAI,EAAQ,CACV,IAAM,EAAO,MAAM,KAAK,SACtB,CACE,GAAG,KAAK,WAAW,CACnB,MACA,IAAK,CAAC,CAAE,SAAQ,CAAE,CAAE,OAAQ,EAAgB,CAAC,CAC9C,CACD,EACD,CAEK,EAAY,EAAK,KAAM,GAAM,EAAE,SAAW,EAAO,CACjD,EAAa,EAAK,KAAM,GAAM,EAAE,SAAW,EAAe,CAC1D,EAAM,GAAa,EAErB,GAAO,EAAI,QAAU,IAAA,IAAa,EAAI,QAAU,OAClD,EAAW,EAAI,WAEZ,CAGL,IAAM,GAAQ,MAFK,KAAK,SAAS,CAAE,GAAG,KAAK,WAAW,CAAE,MAAK,OAAQ,EAAgB,CAAE,EAAE,EAEtE,GACf,GAAS,EAAM,QAAU,IAAA,IAAa,EAAM,QAAU,OACxD,EAAW,EAAM,OAQrB,OAJI,IAAa,IAAA,IAAa,GAAU,YAAa,GAAU,EAAO,UAAY,IAAA,KAChF,EAAW,EAAO,SAGZ,GAAY,KAUtB,MAAM,OAAO,EAAsD,CACjE,IAAM,EAAS,GAAS,QAAU,KAElC,KAAK,QAAQ,MAAM,CAAE,UAAW,KAAK,WAAW,UAAW,SAAQ,CAAE,uBAAuB,CAE5F,IAAM,EAAO,EACT,MAAM,KAAK,SACT,CACE,GAAG,KAAK,WAAW,CACnB,IAAK,CAAC,CAAE,OAAQ,EAAgB,CAAE,CAAE,SAAQ,CAAC,CAC9C,CACD,IACD,CACD,MAAM,KAAK,SAAS,CAAE,GAAG,KAAK,WAAW,CAAE,OAAQ,EAAgB,CAAE,IAAK,CAGxE,EAAS,IAAI,IACnB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAQ,EAAO,IAAI,EAAI,IAAI,EAAI,EAAE,CACnC,EAAI,SAAA,WACN,EAAM,QAAU,EAAI,MAEpB,EAAM,OAAS,EAAI,MAErB,EAAO,IAAI,EAAI,IAAK,EAAM,CAG5B,IAAM,EAAkC,EAAE,CAC1C,IAAK,GAAM,CAAC,EAAK,KAAW,OAAO,QAAQ,KAAK,WAAW,OAAO,CAAE,CAClE,IAAM,EAAQ,EAAO,IAAI,EAAI,CAGzB,EACA,EAAO,cAAgB,GAAU,GAAO,SAAW,IAAA,IAAa,GAAO,SAAW,KACpF,EAAQ,EAAM,OACL,GAAO,UAAY,IAAA,IAAa,GAAO,UAAY,OAC5D,EAAQ,EAAM,SAGZ,IAAU,IAAA,GAEH,YAAa,GAAU,EAAO,UAAY,IAAA,GACnD,EAAO,GAAO,EAAO,QAErB,EAAO,GAAO,KAJd,EAAO,GAAO,EAQlB,OAAO,EAST,MAAM,IACJ,EACA,EACA,EACe,CACf,IAAM,EAAS,KAAK,cAAc,EAAK,EAAQ,CAE/C,KAAK,QAAQ,KAAK,CAAE,UAAW,KAAK,WAAW,UAAW,MAAK,SAAQ,CAAE,gBAAgB,CAEzF,KAAK,eAAe,EAAI,CACxB,IAAM,EAAY,KAAK,WAAW,GAClC,GAAI,EACF,GAAI,CACF,EAAU,MAAM,EAAM,OACf,EAAK,CAMZ,MALI,aAAe,EACX,IAAI,EACR,EAAI,OAAO,IAAK,IAAW,CAAE,GAAG,EAAO,KAAM,CAAC,EAAK,GAAG,EAAM,KAAK,CAAE,EAAE,CACtE,CAEG,EAIV,MAAM,KAAK,UAAU,EAAK,EAAO,EAAQ,KAAK,MAAM,CAUtD,MAAM,QAAQ,EAAsC,EAAuC,CACzF,KAAK,QAAQ,KACX,CAAE,UAAW,KAAK,WAAW,UAAW,KAAM,OAAO,KAAK,EAAO,CAAE,OAAQ,GAAS,OAAQ,CAC5F,0BACD,CAUD,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAO,CAAE,CAEjD,GADA,KAAK,eAAe,EAAI,CACpB,IAAU,IAAA,GAAW,SACzB,IAAM,EAAY,KAAK,WAAW,GAC7B,KACL,GAAI,CACF,EAAU,MAAM,EAAM,OACf,EAAK,CAMZ,MALI,aAAe,EACX,IAAI,EACR,EAAI,OAAO,IAAK,IAAW,CAAE,GAAG,EAAO,KAAM,CAAC,EAAK,GAAG,EAAM,KAAK,CAAE,EAAE,CACtE,CAEG,GAIV,MAAM,KAAK,MAAM,YAAY,KAAO,IAAO,CACzC,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAO,CAAE,CACjD,GAAI,IAAU,IAAA,GAAW,SACzB,IAAM,EAAS,KAAK,cAAc,EAAK,EAAQ,CAC/C,MAAM,KAAK,UAAU,EAAK,EAAO,EAAQ,EAAG,GAE9C,CAOJ,MAAM,OAAmC,EAAQ,EAAuC,CACtF,IAAM,EAAS,KAAK,cAAc,EAAK,EAAQ,CAE/C,KAAK,QAAQ,KAAK,CAAE,UAAW,KAAK,WAAW,UAAW,MAAK,SAAQ,CAAE,mBAAmB,CAE5F,MAAM,KAAK,MAAM,WAAW,CAC1B,GAAG,KAAK,WAAW,CACnB,MACA,SACD,CAAC,CAMJ,MAAM,IAAgC,EAAQ,EAA0C,CACtF,IAAM,EAAS,KAAK,cAAc,EAAK,EAAQ,CAE/C,OAAO,MAAM,KAAK,MAAM,OAAO,CAC7B,GAAG,KAAK,WAAW,CACnB,MACA,SACD,CAAC,CAYJ,eAAuB,EAAmB,CACxC,GAAI,EAAE,KAAO,KAAK,WAAW,QAK3B,MAJA,KAAK,QAAQ,KACX,CAAE,UAAW,KAAK,WAAW,UAAW,MAAK,CAC7C,+BACD,CACS,MAAM,sBAAsB,CAS1C,cAAsB,EAAa,EAAgC,CAKjE,OAJe,KAAK,WAAW,OAAO,IAC1B,cAAgB,GAAS,OAC5B,EAAQ,OAEV,EAIT,WAAoB,CAClB,MAAO,CACL,UAAW,KAAK,WAAW,UAC3B,MAAO,KAAK,MACZ,QAAS,KAAK,QACf,CAOH,MAAc,SAAS,EAAkB,EAAuC,CAC9E,OAAQ,MAAM,KAAK,MAAM,SAAS,CAAE,QAAO,QAAO,CAAC,CAQrD,MAAc,UAAU,EAAa,EAAgB,EAAgB,EAA0B,CAC7F,MAAM,EAAY,OAChB,CACE,UAAW,KAAK,WAAW,UAC3B,MAAO,KAAK,MACZ,QAAS,KAAK,QACd,MACA,SACA,QACA,UAAW,IAAI,KAChB,CACD,CACE,OAAQ,CAAC,YAAa,QAAS,UAAW,MAAO,SAAS,CAC1D,IAAK,CACH,QACA,UAAW,IAAI,KAChB,CACF,CACF,GCtWL,SAAgB,EACd,EACA,EACmB,CACnB,IAAM,EAAM,GAAS,KAAO,GAAQ,CAEpC,OAAO,IAAI,EAAe,EAAY,CACpC,GAAI,EAAI,GAAG,UACX,OAAQ,EAAI,OAAO,MAAM,CAAE,SAAU,EAAW,UAAW,CAAC,CAC5D,MAAO,GAAS,MAChB,QAAS,GAAS,QACnB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as InferSettingValue, d as SettingConfig,
|
|
1
|
+
import { a as InferSettingValue, d as SettingConfig, h as SettingsDefinition, o as InferSettingsMap, p as SettingScope } from "./types-C_SUmm7Q.mjs";
|
|
2
2
|
import { Logger, ToolkitApp } from "@murumets-ee/core";
|
|
3
3
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
4
|
|
|
@@ -7,11 +7,11 @@ interface SettingsClientConfig {
|
|
|
7
7
|
/** Database client (read-write) */
|
|
8
8
|
db: PostgresJsDatabase;
|
|
9
9
|
/** Logger instance */
|
|
10
|
-
logger?: Logger;
|
|
10
|
+
logger?: Logger | undefined;
|
|
11
11
|
/** Override scope (defaults to definition's scope) */
|
|
12
|
-
scope?: SettingScope;
|
|
12
|
+
scope?: SettingScope | undefined;
|
|
13
13
|
/** Scope ID (required for team/user scope, defaults to '__global__' for global) */
|
|
14
|
-
scopeId?: string;
|
|
14
|
+
scopeId?: string | undefined;
|
|
15
15
|
}
|
|
16
16
|
interface LocaleOption {
|
|
17
17
|
/** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */
|
|
@@ -64,6 +64,12 @@ declare class SettingsClient<S extends Record<string, SettingConfig> = Record<st
|
|
|
64
64
|
* Check if a setting has a stored value (not relying on default).
|
|
65
65
|
*/
|
|
66
66
|
has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Reject keys not declared in this namespace's schema.
|
|
69
|
+
* Generic message — does NOT echo the key or namespace back to callers,
|
|
70
|
+
* to avoid leaking schema structure on the admin route's error path.
|
|
71
|
+
*/
|
|
72
|
+
private assertKnownKey;
|
|
67
73
|
/**
|
|
68
74
|
* Resolve which locale to use for a given key.
|
|
69
75
|
* Non-translatable settings always use DEFAULT_LOCALE.
|
|
@@ -72,6 +78,11 @@ declare class SettingsClient<S extends Record<string, SettingConfig> = Record<st
|
|
|
72
78
|
private resolveLocale;
|
|
73
79
|
/** Base where conditions: namespace + scope + scopeId. */
|
|
74
80
|
private baseWhere;
|
|
81
|
+
/**
|
|
82
|
+
* Typed wrapper around `table.findMany` — concentrates the cast that bridges
|
|
83
|
+
* the wide TableClient row type to our narrow `SettingsRow` view.
|
|
84
|
+
*/
|
|
85
|
+
private findRows;
|
|
75
86
|
/**
|
|
76
87
|
* Upsert a single setting row via the TableClient.
|
|
77
88
|
* Accepts either the top-level table client or a transactional one —
|
|
@@ -83,11 +94,11 @@ declare class SettingsClient<S extends Record<string, SettingConfig> = Record<st
|
|
|
83
94
|
//#region src/client-factory.d.ts
|
|
84
95
|
interface CreateSettingsClientOptions {
|
|
85
96
|
/** Override the toolkit app (defaults to getApp()) */
|
|
86
|
-
app?: ToolkitApp;
|
|
97
|
+
app?: ToolkitApp | undefined;
|
|
87
98
|
/** Override scope from definition */
|
|
88
|
-
scope?: SettingScope;
|
|
99
|
+
scope?: SettingScope | undefined;
|
|
89
100
|
/** Scope ID for team/user scoped settings */
|
|
90
|
-
scopeId?: string;
|
|
101
|
+
scopeId?: string | undefined;
|
|
91
102
|
}
|
|
92
103
|
/**
|
|
93
104
|
* Create a typed SettingsClient.
|
|
@@ -104,4 +115,4 @@ interface CreateSettingsClientOptions {
|
|
|
104
115
|
declare function createSettingsClient<S extends Record<string, SettingConfig>>(definition: SettingsDefinition<S>, options?: CreateSettingsClientOptions): SettingsClient<S>;
|
|
105
116
|
//#endregion
|
|
106
117
|
export { SettingsClientConfig as a, SettingsClient as i, createSettingsClient as n, LocaleOption as r, CreateSettingsClientOptions as t };
|
|
107
|
-
//# sourceMappingURL=client-factory-
|
|
118
|
+
//# sourceMappingURL=client-factory-BTSJWdrk.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-factory-BTSJWdrk.d.mts","names":[],"sources":["../src/client.ts","../src/client-factory.ts"],"mappings":";;;;;UA4CiB,oBAAA;EA4B0C;EA1BzD,EAAA,EAAI,kBAAA;EAmCuC;EAjC3C,MAAA,GAAS,MAAA;EAiC8C;EA/BvD,KAAA,GAAQ,YAAA;EA0DD;EAxDP,OAAA;AAAA;AAAA,UAGe,YAAA;EAuDJ;EArDX,MAAA;AAAA;AAAA,cAcW,cAAA,WACD,MAAA,SAAe,aAAA,IAAiB,MAAA,SAAe,aAAA;EAAA,QAEjD,UAAA;EAAA,QACA,KAAA;EAAA,QACA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,OAAA;EAAA,QACA,UAAA;cAEI,UAAA,EAAY,kBAAA,CAAmB,CAAA,GAAI,MAAA,EAAQ,oBAAA;EAyI3C;;;;;;EA/GN,GAAA,0BAA6B,CAAA,CAAA,CACjC,GAAA,EAAK,CAAA,EACL,OAAA,GAAU,YAAA,GACT,OAAA,CAAQ,iBAAA,CAAkB,CAAA,CAAE,CAAA;EAuLO;;;;;;;EAvIhC,MAAA,CAAO,OAAA,GAAU,YAAA,GAAe,OAAA,CAAQ,gBAAA,CAAiB,CAAA;EAsJe;;;;;;EA7FxE,GAAA,0BAA6B,CAAA,CAAA,CACjC,GAAA,EAAK,CAAA,EACL,KAAA,EAAO,iBAAA,CAAkB,CAAA,CAAE,CAAA,IAC3B,OAAA,GAAU,YAAA,GACT,OAAA;EAjJK;;;;;;;EA+KF,OAAA,CAAQ,MAAA,EAAQ,OAAA,CAAQ,gBAAA,CAAiB,CAAA,IAAK,OAAA,GAAU,YAAA,GAAe,OAAA;EAxKlC;;;;EAoNrC,MAAA,0BAAgC,CAAA,CAAA,CAAG,GAAA,EAAK,CAAA,EAAG,OAAA,GAAU,YAAA,GAAe,OAAA;EA1LhE;;;EAyMJ,GAAA,0BAA6B,CAAA,CAAA,CAAG,GAAA,EAAK,CAAA,EAAG,OAAA,GAAU,YAAA,GAAe,OAAA;EAvM3D;;;;;EAAA,QA0NJ,cAAA;EAzKF;;;;;EAAA,QAwLE,aAAA;EA/HF;EAAA,QAwIE,SAAA;EAxI2B;;;;EAAA,QAoJrB,QAAA;EAlJe;;;;;EAAA,QA2Jf,SAAA;AAAA;;;UCzWC,2BAAA;EDqCN;ECnCT,GAAA,GAAM,UAAA;EDqCc;ECnCpB,KAAA,GAAQ,YAAA;ED+BR;EC7BA,OAAA;AAAA;;;;;;;ADsCF;;;;;AAgBA;iBCvCgB,oBAAA,WAA+B,MAAA,SAAe,aAAA,EAAA,CAC5D,UAAA,EAAY,kBAAA,CAAmB,CAAA,GAC/B,OAAA,GAAU,2BAAA,GACT,cAAA,CAAe,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{getApp as e}from"@murumets-ee/core";import{ZodError as t,z as n}from"zod";import{column as r,defineTable as i}from"@murumets-ee/db";const a=i({name:`toolkit_settings`,columns:{id:r.uuid({primaryKey:!0,defaultRandom:!0}),namespace:r.varchar({length:100,notNull:!0}),scope:r.varchar({length:20,notNull:!0,default:`global`}),scopeId:r.varchar({length:100,notNull:!0,default:`__global__`,pgName:`scope_id`}),key:r.varchar({length:255,notNull:!0}),locale:r.varchar({length:10,notNull:!0,default:`_default`}),value:r.jsonb(),updatedAt:r.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`}),updatedBy:r.varchar({length:255,pgName:`updated_by`})},unique:[{on:[`namespace`,`scope`,`scopeId`,`key`,`locale`]}]});a.table,i({name:`toolkit_view_state`,columns:{id:r.uuid({primaryKey:!0,defaultRandom:!0}),userId:r.varchar({length:255,notNull:!0,pgName:`user_id`}),viewKey:r.varchar({length:255,notNull:!0,pgName:`view_key`}),state:r.jsonb({notNull:!0}),expiresAt:r.timestamp({withTimezone:!0,pgName:`expires_at`}),updatedAt:r.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`userId`,`viewKey`]}]}).table;const o=`_default`;function s(e){switch(e.type){case`text`:{let t=n.string();return t=t.max(e.maxLength??65536),e.minLength&&(t=t.min(e.minLength)),e.pattern&&(t=t.regex(e.pattern)),t}case`number`:{let t=n.number();return e.integer&&(t=t.int()),e.min!==void 0&&(t=t.min(e.min)),e.max!==void 0&&(t=t.max(e.max)),t}case`boolean`:return n.boolean();case`select`:return n.enum(e.options);case`json`:return e.schema??n.unknown();case`media`:return n.string().uuid();default:return n.unknown()}}function c(e){let t={};for(let[n,r]of Object.entries(e.schema)){let e=s(r);`default`in r&&r.default!==void 0||(e=e.nullable()),t[n]=e}return t}var l=class{definition;table;logger;scope;scopeId;validators;constructor(e,t){if(typeof window<`u`)throw Error(`SettingsClient cannot be used in browser code.`);if(this.definition=e,this.table=a.makeClient(t.db),this.logger=t.logger,this.scope=t.scope??e.scope,this.scopeId=t.scopeId??`__global__`,(this.scope===`team`||this.scope===`user`)&&!t.scopeId)throw Error(`scopeId is required for ${this.scope}-scoped settings (namespace: ${e.namespace})`);this.validators=c(e)}async get(e,t){let n=this.definition.schema[e],r=n?.translatable&&t?.locale?t.locale:null;this.logger?.debug({namespace:this.definition.namespace,key:e,locale:r},`Getting setting`);let i;if(r){let t=await this.findRows({...this.baseWhere(),key:e,$or:[{locale:r},{locale:o}]},2),n=t.find(e=>e.locale===r),a=t.find(e=>e.locale===o),s=n??a;s&&s.value!==void 0&&s.value!==null&&(i=s.value)}else{let t=(await this.findRows({...this.baseWhere(),key:e,locale:o},1))[0];t&&t.value!==void 0&&t.value!==null&&(i=t.value)}return i===void 0&&n&&`default`in n&&n.default!==void 0&&(i=n.default),i??null}async getAll(e){let t=e?.locale??null;this.logger?.debug({namespace:this.definition.namespace,locale:t},`Getting all settings`);let n=t?await this.findRows({...this.baseWhere(),$or:[{locale:o},{locale:t}]},1e3):await this.findRows({...this.baseWhere(),locale:o},1e3),r=new Map;for(let e of n){let t=r.get(e.key)??{};e.locale===`_default`?t.default=e.value:t.locale=e.value,r.set(e.key,t)}let i={};for(let[e,n]of Object.entries(this.definition.schema)){let a=r.get(e),o;n.translatable&&t&&a?.locale!==void 0&&a?.locale!==null?o=a.locale:a?.default!==void 0&&a?.default!==null&&(o=a.default),o===void 0?`default`in n&&n.default!==void 0?i[e]=n.default:i[e]=null:i[e]=o}return i}async set(e,n,r){let i=this.resolveLocale(e,r);this.logger?.info({namespace:this.definition.namespace,key:e,locale:i},`Setting value`),this.assertKnownKey(e);let a=this.validators[e];if(a)try{a.parse(n)}catch(n){throw n instanceof t?new t(n.issues.map(t=>({...t,path:[e,...t.path]}))):n}await this.upsertRow(e,n,i,this.table)}async setMany(e,n){this.logger?.info({namespace:this.definition.namespace,keys:Object.keys(e),locale:n?.locale},`Setting multiple values`);for(let[n,r]of Object.entries(e)){if(this.assertKnownKey(n),r===void 0)continue;let e=this.validators[n];if(e)try{e.parse(r)}catch(e){throw e instanceof t?new t(e.issues.map(e=>({...e,path:[n,...e.path]}))):e}}await this.table.transaction(async t=>{for(let[r,i]of Object.entries(e)){if(i===void 0)continue;let e=this.resolveLocale(r,n);await this.upsertRow(r,i,e,t)}})}async delete(e,t){let n=this.resolveLocale(e,t);this.logger?.info({namespace:this.definition.namespace,key:e,locale:n},`Deleting setting`),await this.table.deleteMany({...this.baseWhere(),key:e,locale:n})}async has(e,t){let n=this.resolveLocale(e,t);return await this.table.exists({...this.baseWhere(),key:e,locale:n})}assertKnownKey(e){if(!(e in this.definition.schema))throw this.logger?.warn({namespace:this.definition.namespace,key:e},`Rejected unknown setting key`),Error(`Unknown setting key`)}resolveLocale(e,t){return this.definition.schema[e]?.translatable&&t?.locale?t.locale:o}baseWhere(){return{namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId}}async findRows(e,t){return await this.table.findMany({where:e,limit:t})}async upsertRow(e,t,n,r){await r.upsert({namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId,key:e,locale:n,value:t,updatedAt:new Date},{target:[`namespace`,`scope`,`scopeId`,`key`,`locale`],set:{value:t,updatedAt:new Date}})}};function u(t,n){let r=n?.app??e();return new l(t,{db:r.db.readWrite,logger:r.logger.child({settings:t.namespace}),scope:n?.scope,scopeId:n?.scopeId})}export{u as createSettingsClient};
|
|
2
|
+
//# sourceMappingURL=client-factory-C44mk_oR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-factory-C44mk_oR.mjs","names":[],"sources":["../src/schema.ts","../src/types.ts","../src/validation.ts","../src/client.ts","../src/client-factory.ts"],"sourcesContent":["/**\n * Drizzle schema for settings tables.\n *\n * Two tables:\n * - toolkit_settings: typed key-value settings grouped by namespace and scope\n * - toolkit_view_state: schemaless user-scoped JSON blobs with optional TTL\n *\n * These tables are registered for migration discovery via the `settings()`\n * plugin's `tables` field. `lumi migrate` picks them up automatically.\n *\n * Built on `@murumets-ee/db`'s `defineTable` — the raw `pgTable` is still\n * re-exported (via `.table`) for backwards compatibility with existing\n * imports and tests, but all query construction in SettingsClient /\n * ViewStateClient goes through the typed TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\n/**\n * Typed settings table.\n *\n * Stores key-value pairs grouped by namespace and scoped\n * to global, team, or user contexts.\n *\n * scopeId uses '__global__' sentinel for global scope to avoid\n * PostgreSQL's NULL != NULL behavior in unique constraints.\n */\nexport const toolkitSettingsTable = defineTable({\n name: 'toolkit_settings',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n namespace: column.varchar({ length: 100, notNull: true }),\n scope: column.varchar({ length: 20, notNull: true, default: 'global' }),\n scopeId: column.varchar({\n length: 100,\n notNull: true,\n default: '__global__',\n pgName: 'scope_id',\n }),\n key: column.varchar({ length: 255, notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_default' }),\n value: column.jsonb(),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n updatedBy: column.varchar({ length: 255, pgName: 'updated_by' }),\n },\n unique: [{ on: ['namespace', 'scope', 'scopeId', 'key', 'locale'] }],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitSettings = toolkitSettingsTable.table\n\n/**\n * View state table.\n *\n * Stores schemaless user-scoped JSON blobs for persisting\n * UI state (table filters, column order, etc.) with optional TTL.\n */\nexport const toolkitViewStateTable = defineTable({\n name: 'toolkit_view_state',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n userId: column.varchar({ length: 255, notNull: true, pgName: 'user_id' }),\n viewKey: column.varchar({ length: 255, notNull: true, pgName: 'view_key' }),\n state: column.jsonb({ notNull: true }),\n expiresAt: column.timestamp({ withTimezone: true, pgName: 'expires_at' }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['userId', 'viewKey'] }],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitViewState = toolkitViewStateTable.table\n","/**\n * Setting configuration types and compile-time type inference.\n *\n * Design mirrors the entity field system:\n * - Config interfaces define what each setting type accepts\n * - SettingToTS maps a single config to its TypeScript type\n * - InferSettingValue adds null awareness based on `default` presence\n * - InferSettingsMap maps an entire schema to a typed record\n */\n\nimport type { ZodType } from 'zod'\n\n// ---------------------------------------------------------------\n// 1. Setting config interfaces\n// ---------------------------------------------------------------\n\nexport interface BaseSettingConfig {\n /** Human-readable label for admin UI */\n label?: string\n /** Description / help text */\n description?: string\n /** If true, this setting can have per-locale values (mirrors entity translatable pattern) */\n translatable?: boolean\n}\n\nexport interface TextSettingConfig extends BaseSettingConfig {\n type: 'text'\n default?: string\n maxLength?: number\n minLength?: number\n pattern?: RegExp\n /** Render as textarea instead of single-line input. */\n multiline?: boolean\n}\n\nexport interface NumberSettingConfig extends BaseSettingConfig {\n type: 'number'\n default?: number\n min?: number\n max?: number\n integer?: boolean\n}\n\nexport interface BooleanSettingConfig extends BaseSettingConfig {\n type: 'boolean'\n default?: boolean\n}\n\nexport interface SelectSettingConfig<O extends readonly string[] = readonly string[]>\n extends BaseSettingConfig {\n type: 'select'\n options: O\n default?: O[number]\n}\n\nexport interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {\n type: 'json'\n default?: T\n /** Optional Zod schema for validation. If provided, values are validated on set. */\n schema?: ZodType<T>\n /**\n * Optional renderer slot key. When set, the settings form looks up\n * `renderers[renderer]` (provided by the admin shell) and renders that\n * component in place of the default JSON textarea. Falls back to the\n * JSON renderer if the slot key is absent from the renderer map.\n *\n * The component receives `{ value, onChange, error }` and is responsible\n * for editing the field's value. Convention: prefix with the namespace\n * (e.g. `'media.imageStyles'`, `'commerce.searchRegistry'`) to avoid\n * collisions across plugins.\n */\n renderer?: string\n}\n\nexport interface MediaSettingConfig extends BaseSettingConfig {\n type: 'media'\n default?: string\n accept?: string[]\n}\n\nexport type SettingConfig =\n | TextSettingConfig\n | NumberSettingConfig\n | BooleanSettingConfig\n | SelectSettingConfig\n | JsonSettingConfig\n | MediaSettingConfig\n\n// ---------------------------------------------------------------\n// 2. Setting-to-TypeScript mapping (single setting)\n// ---------------------------------------------------------------\n\n/**\n * Maps a single SettingConfig to its TypeScript output type.\n * Each branch is a shallow comparison — no recursion.\n */\nexport type SettingToTS<S extends SettingConfig> = S extends TextSettingConfig\n ? string\n : S extends NumberSettingConfig\n ? number\n : S extends BooleanSettingConfig\n ? boolean\n : S extends SelectSettingConfig<infer O>\n ? O[number]\n : S extends JsonSettingConfig<infer T>\n ? T\n : S extends MediaSettingConfig\n ? string\n : never\n\n// ---------------------------------------------------------------\n// 3. Null awareness: settings with defaults always return value\n// ---------------------------------------------------------------\n\n/**\n * If a setting has a `default`, get() never returns null.\n * Without a default, it returns T | null.\n */\nexport type InferSettingValue<S extends SettingConfig> = S extends { default: unknown }\n ? SettingToTS<S>\n : SettingToTS<S> | null\n\n// ---------------------------------------------------------------\n// 4. Full settings map type (what getAll() returns)\n// ---------------------------------------------------------------\n\nexport type InferSettingsMap<Schema extends Record<string, SettingConfig>> = {\n [K in keyof Schema]: InferSettingValue<Schema[K]>\n}\n\n// ---------------------------------------------------------------\n// 5. Scope types\n// ---------------------------------------------------------------\n\nexport type SettingScope = 'global' | 'team' | 'user'\n\n/** Sentinel value for global scope_id (avoids NULL uniqueness issues) */\nexport const GLOBAL_SCOPE_ID = '__global__'\n\n/** Sentinel value for locale column when no locale is specified (base/default value) */\nexport const DEFAULT_LOCALE = '_default'\n\n// ---------------------------------------------------------------\n// 6. Settings definition (returned by defineSettings)\n// ---------------------------------------------------------------\n\n/**\n * Props every custom JSON field renderer (`renderer` slot) receives.\n *\n * `value` and `onChange` plug straight into react-hook-form's Controller;\n * `error` carries the field-level error message (from Zod or server-side\n * field errors) so the renderer can surface inline feedback without\n * needing access to the form context.\n *\n * Plugins that ship a renderer write their component against\n * `SettingFieldRendererProps<TheirValueType>` and contribute it via\n * `Plugin.adminUi.settingRenderers`. The framework dispatches with\n * `value: unknown` at the storage boundary; type narrowing is the\n * plugin author's responsibility (the same author owns the schema, so\n * the value shape is a known contract).\n *\n * Type lives here (not in admin-ui) to keep plugin packages free of an\n * `@murumets-ee/admin-ui` dependency, which would create a cycle.\n */\nexport interface SettingFieldRendererProps<T = unknown> {\n value: T\n onChange: (value: T) => void\n error?: string | undefined\n}\n\nexport interface SettingsDefinition<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n /** Unique namespace for this settings group */\n namespace: string\n /** Default scope for these settings */\n scope: SettingScope\n /** Setting schema (the shape) */\n schema: S\n /** Human-readable label for admin UI */\n label?: string\n /**\n * Lucide icon name (e.g. `'globe'`, `'ticket'`) for the sidebar nav\n * entry this definition produces. Falls back to `'settings'` when\n * unset.\n */\n iconName?: string\n /**\n * Hide this namespace from the auto-generated sidebar nav. Still\n * renders in the settings API. Useful for internal-only namespaces\n * like the permissions store.\n */\n hideFromMenu?: boolean\n}\n","/**\n * Generate Zod validation schemas from setting config definitions.\n * Validates values before writing to the database.\n */\n\nimport { z } from 'zod'\nimport type { SettingConfig, SettingsDefinition } from './types.js'\n\n/**\n * Default cap for text settings without an explicit `maxLength`.\n * 64 KiB — defense-in-depth against an admin uploading multi-MB blobs into\n * a `varchar`/`text`-shaped setting.\n */\nconst DEFAULT_TEXT_MAX_LENGTH = 65_536\n\n/**\n * Convert a single setting config to a Zod schema.\n */\nexport function settingToZod(config: SettingConfig): z.ZodType {\n switch (config.type) {\n case 'text': {\n let schema: z.ZodString = z.string()\n schema = schema.max(config.maxLength ?? DEFAULT_TEXT_MAX_LENGTH)\n if (config.minLength) schema = schema.min(config.minLength)\n if (config.pattern) schema = schema.regex(config.pattern)\n return schema\n }\n\n case 'number': {\n let schema: z.ZodNumber = z.number()\n if (config.integer) schema = schema.int()\n if (config.min !== undefined) schema = schema.min(config.min)\n if (config.max !== undefined) schema = schema.max(config.max)\n return schema\n }\n\n case 'boolean':\n return z.boolean()\n\n case 'select':\n return z.enum(config.options as [string, ...string[]])\n\n case 'json':\n return config.schema ?? z.unknown()\n\n case 'media':\n return z.string().uuid()\n\n default:\n return z.unknown()\n }\n}\n\n/**\n * Generate a validation map for all settings in a definition.\n * Returns a Record<key, ZodType> for validating individual set() calls.\n *\n * Settings without a default are nullable (matching InferSettingValue),\n * so their validators accept null.\n */\nexport function generateSettingValidators(\n definition: SettingsDefinition,\n): Record<string, z.ZodType> {\n const validators: Record<string, z.ZodType> = {}\n for (const [key, config] of Object.entries(definition.schema)) {\n let schema = settingToZod(config)\n const hasDefault = 'default' in config && config.default !== undefined\n if (!hasDefault) {\n schema = schema.nullable()\n }\n validators[key] = schema\n }\n return validators\n}\n","/**\n * SettingsClient — typed CRUD for key-value settings.\n *\n * Server-only. Uses read-write DB connection.\n * Validates values against the setting schema on set().\n *\n * Supports per-locale values for settings marked `translatable: true`.\n * Non-translatable settings always use the `__default__` locale.\n *\n * All persistence goes through the `toolkit_settings` TableClient\n * (`defineTable`) — no direct Drizzle usage from this module.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n *\n * // Default locale\n * const name = await settings.get('siteName') // string (has default)\n *\n * // Locale-specific (only for translatable settings)\n * const nameEt = await settings.get('siteName', { locale: 'et' })\n *\n * await settings.set('siteName', 'Mänguväljak', { locale: 'et' })\n * ```\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { WhereClause } from '@murumets-ee/db'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { type ZodType, ZodError } from 'zod'\nimport { toolkitSettingsTable } from './schema.js'\nimport type {\n InferSettingsMap,\n InferSettingValue,\n SettingConfig,\n SettingScope,\n SettingsDefinition,\n} from './types.js'\nimport { DEFAULT_LOCALE, GLOBAL_SCOPE_ID } from './types.js'\nimport { generateSettingValidators } from './validation.js'\n\nexport interface SettingsClientConfig {\n /** Database client (read-write) */\n db: PostgresJsDatabase\n /** Logger instance */\n logger?: Logger | undefined\n /** Override scope (defaults to definition's scope) */\n scope?: SettingScope | undefined\n /** Scope ID (required for team/user scope, defaults to '__global__' for global) */\n scopeId?: string | undefined\n}\n\nexport interface LocaleOption {\n /** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */\n locale?: string\n}\n\n/** Row shape returned by the settings table (for narrow typing of fetched rows). */\ninterface SettingsRow {\n key: string\n locale: string\n value: unknown\n}\n\ntype TableClient = ReturnType<typeof toolkitSettingsTable.makeClient>\ntype SettingsTableColumns = (typeof toolkitSettingsTable)['schema']['columns']\ntype FindWhere = WhereClause<SettingsTableColumns>\n\nexport class SettingsClient<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n private definition: SettingsDefinition<S>\n private table: TableClient\n private logger?: Logger | undefined\n private scope: SettingScope\n private scopeId: string\n private validators: Record<string, ZodType>\n\n constructor(definition: SettingsDefinition<S>, config: SettingsClientConfig) {\n if (typeof window !== 'undefined') {\n throw new Error('SettingsClient cannot be used in browser code.')\n }\n\n this.definition = definition\n this.table = toolkitSettingsTable.makeClient(config.db)\n this.logger = config.logger\n this.scope = config.scope ?? definition.scope\n this.scopeId = config.scopeId ?? GLOBAL_SCOPE_ID\n\n if ((this.scope === 'team' || this.scope === 'user') && !config.scopeId) {\n throw new Error(\n `scopeId is required for ${this.scope}-scoped settings (namespace: ${definition.namespace})`,\n )\n }\n\n this.validators = generateSettingValidators(definition)\n }\n\n /**\n * Get a single setting value.\n *\n * For translatable settings with a locale, tries locale-specific value first,\n * then falls back to the default value, then the schema default, then null.\n */\n async get<K extends string & keyof S>(\n key: K,\n options?: LocaleOption,\n ): Promise<InferSettingValue<S[K]>> {\n const config = this.definition.schema[key]\n const locale = config?.translatable && options?.locale ? options.locale : null\n\n this.logger?.debug({ namespace: this.definition.namespace, key, locale }, 'Getting setting')\n\n let resolved: unknown\n\n if (locale) {\n const rows = await this.findRows(\n {\n ...this.baseWhere(),\n key,\n $or: [{ locale }, { locale: DEFAULT_LOCALE }],\n },\n 2,\n )\n\n const localeRow = rows.find((r) => r.locale === locale)\n const defaultRow = rows.find((r) => r.locale === DEFAULT_LOCALE)\n const row = localeRow ?? defaultRow\n\n if (row && row.value !== undefined && row.value !== null) {\n resolved = row.value\n }\n } else {\n const rows = await this.findRows({ ...this.baseWhere(), key, locale: DEFAULT_LOCALE }, 1)\n\n const first = rows[0]\n if (first && first.value !== undefined && first.value !== null) {\n resolved = first.value\n }\n }\n\n if (resolved === undefined && config && 'default' in config && config.default !== undefined) {\n resolved = config.default\n }\n\n return (resolved ?? null) as InferSettingValue<S[K]>\n }\n\n /**\n * Get all settings for this namespace/scope as a typed object.\n * Missing values are filled from schema defaults.\n *\n * When locale is specified, translatable settings prefer the locale-specific\n * value over the default value.\n */\n async getAll(options?: LocaleOption): Promise<InferSettingsMap<S>> {\n const locale = options?.locale ?? null\n\n this.logger?.debug({ namespace: this.definition.namespace, locale }, 'Getting all settings')\n\n const rows = locale\n ? await this.findRows(\n {\n ...this.baseWhere(),\n $or: [{ locale: DEFAULT_LOCALE }, { locale }],\n },\n 1000,\n )\n : await this.findRows({ ...this.baseWhere(), locale: DEFAULT_LOCALE }, 1000)\n\n // Build lookup: key → { default: value, locale: value }\n const stored = new Map<string, { default?: unknown; locale?: unknown }>()\n for (const row of rows) {\n const entry = stored.get(row.key) ?? {}\n if (row.locale === DEFAULT_LOCALE) {\n entry.default = row.value\n } else {\n entry.locale = row.value\n }\n stored.set(row.key, entry)\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, config] of Object.entries(this.definition.schema)) {\n const entry = stored.get(key)\n\n // For translatable settings with locale, prefer locale-specific value\n let value: unknown\n if (config.translatable && locale && entry?.locale !== undefined && entry?.locale !== null) {\n value = entry.locale\n } else if (entry?.default !== undefined && entry?.default !== null) {\n value = entry.default\n }\n\n if (value !== undefined) {\n result[key] = value\n } else if ('default' in config && config.default !== undefined) {\n result[key] = config.default\n } else {\n result[key] = null\n }\n }\n\n return result as InferSettingsMap<S>\n }\n\n /**\n * Set a single setting value.\n * Validates against the schema before writing.\n *\n * Pass `{ locale }` to write a locale-specific value (only for translatable settings).\n */\n async set<K extends string & keyof S>(\n key: K,\n value: InferSettingValue<S[K]>,\n options?: LocaleOption,\n ): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Setting value')\n\n this.assertKnownKey(key)\n const validator = this.validators[key]\n if (validator) {\n try {\n validator.parse(value)\n } catch (err) {\n if (err instanceof ZodError) {\n throw new ZodError(\n err.issues.map((issue) => ({ ...issue, path: [key, ...issue.path] })),\n )\n }\n throw err\n }\n }\n\n await this.upsertRow(key, value, locale, this.table)\n }\n\n /**\n * Set multiple settings at once (validated individually).\n * Writes in a transaction — all or nothing.\n *\n * Pass `{ locale }` to write locale-specific values for translatable settings.\n * Non-translatable settings in the values will always write to the default locale.\n */\n async setMany(values: Partial<InferSettingsMap<S>>, options?: LocaleOption): Promise<void> {\n this.logger?.info(\n { namespace: this.definition.namespace, keys: Object.keys(values), locale: options?.locale },\n 'Setting multiple values',\n )\n\n // Validate all first (fail fast). Zod's `issue.path` is RELATIVE to\n // the validator we invoke — for a top-level field with a\n // `setting.text({ minLength: 1 })` the path is `[]`; for a nested\n // failure inside a json schema, e.g. `imageStyles[thumb].width`,\n // it's `['thumb', 'width']`. To make the errors useful upstream\n // (the PATCH route turns `path` into a fieldErrors map keyed by the\n // setting name), we prefix each issue with the offending setting key\n // before re-throwing.\n for (const [key, value] of Object.entries(values)) {\n this.assertKnownKey(key)\n if (value === undefined) continue\n const validator = this.validators[key]\n if (!validator) continue\n try {\n validator.parse(value)\n } catch (err) {\n if (err instanceof ZodError) {\n throw new ZodError(\n err.issues.map((issue) => ({ ...issue, path: [key, ...issue.path] })),\n )\n }\n throw err\n }\n }\n\n await this.table.transaction(async (tx) => {\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n const locale = this.resolveLocale(key, options)\n await this.upsertRow(key, value, locale, tx)\n }\n })\n }\n\n /**\n * Delete a setting (resets to default on next get).\n * Pass `{ locale }` to delete only the locale-specific value.\n */\n async delete<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Deleting setting')\n\n await this.table.deleteMany({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n /**\n * Check if a setting has a stored value (not relying on default).\n */\n async has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean> {\n const locale = this.resolveLocale(key, options)\n\n return await this.table.exists({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n // ---------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------\n\n /**\n * Reject keys not declared in this namespace's schema.\n * Generic message — does NOT echo the key or namespace back to callers,\n * to avoid leaking schema structure on the admin route's error path.\n */\n private assertKnownKey(key: string): void {\n if (!(key in this.definition.schema)) {\n this.logger?.warn(\n { namespace: this.definition.namespace, key },\n 'Rejected unknown setting key',\n )\n throw new Error('Unknown setting key')\n }\n }\n\n /**\n * Resolve which locale to use for a given key.\n * Non-translatable settings always use DEFAULT_LOCALE.\n * Translatable settings use the requested locale or DEFAULT_LOCALE.\n */\n private resolveLocale(key: string, options?: LocaleOption): string {\n const config = this.definition.schema[key]\n if (config?.translatable && options?.locale) {\n return options.locale\n }\n return DEFAULT_LOCALE\n }\n\n /** Base where conditions: namespace + scope + scopeId. */\n private baseWhere() {\n return {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n }\n }\n\n /**\n * Typed wrapper around `table.findMany` — concentrates the cast that bridges\n * the wide TableClient row type to our narrow `SettingsRow` view.\n */\n private async findRows(where: FindWhere, limit: number): Promise<SettingsRow[]> {\n return (await this.table.findMany({ where, limit })) as unknown as SettingsRow[]\n }\n\n /**\n * Upsert a single setting row via the TableClient.\n * Accepts either the top-level table client or a transactional one —\n * same shape, used inside `setMany`'s transaction.\n */\n private async upsertRow(key: string, value: unknown, locale: string, tableClient: TableClient) {\n await tableClient.upsert(\n {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n key,\n locale,\n value,\n updatedAt: new Date(),\n },\n {\n target: ['namespace', 'scope', 'scopeId', 'key', 'locale'],\n set: {\n value,\n updatedAt: new Date(),\n },\n },\n )\n }\n}\n","/**\n * Factory function for creating typed SettingsClient instances.\n *\n * Follows the createAdminClient(entity, app?) pattern from @murumets-ee/core.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { getApp } from '@murumets-ee/core'\nimport { SettingsClient } from './client.js'\nimport type { SettingConfig, SettingScope, SettingsDefinition } from './types.js'\n\nexport interface CreateSettingsClientOptions {\n /** Override the toolkit app (defaults to getApp()) */\n app?: ToolkitApp | undefined\n /** Override scope from definition */\n scope?: SettingScope | undefined\n /** Scope ID for team/user scoped settings */\n scopeId?: string | undefined\n}\n\n/**\n * Create a typed SettingsClient.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n * const name = await settings.get('siteName') // string\n * ```\n */\nexport function createSettingsClient<S extends Record<string, SettingConfig>>(\n definition: SettingsDefinition<S>,\n options?: CreateSettingsClientOptions,\n): SettingsClient<S> {\n const app = options?.app ?? getApp()\n\n return new SettingsClient(definition, {\n db: app.db.readWrite,\n logger: app.logger.child({ settings: definition.namespace }),\n scope: options?.scope,\n scopeId: options?.scopeId,\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,t){return{type:e,...t}}const t={text:t=>e(`text`,t),number:t=>e(`number`,t),boolean:t=>e(`boolean`,t),select:t=>e(`select`,t),json:t=>e(`json`,t),media:t=>e(`media`,t)},n=`__global__`,r=`_default`,i=/^[a-zA-Z](?:[a-zA-Z0-9]|[_.:-][a-zA-Z0-9])*$/;function a(e){if(!e.namespace)throw Error(`Settings namespace is required`);if(!i.test(e.namespace))throw Error(`Settings namespace "${e.namespace}" is invalid: must start with a letter, end with a letter or digit, and use only [a-zA-Z0-9] or single _ . : - separators (no consecutive punctuation).`);if(!e.schema||Object.keys(e.schema).length===0)throw Error(`Settings schema must have at least one setting`);return e}export{t as i,r as n,n as r,a as t};
|
|
2
|
+
//# sourceMappingURL=define-settings-DHaHn6XA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-settings-DHaHn6XA.mjs","names":[],"sources":["../src/builders.ts","../src/types.ts","../src/define-settings.ts"],"sourcesContent":["/**\n * Fluent API for building setting definitions.\n *\n * Each builder uses a `const` generic parameter on the config to preserve\n * literal types (e.g., `default: 'My Site'` stays literal, not `string`).\n * This enables compile-time type inference in the settings system.\n *\n * Pattern matches packages/entity/src/fields/builders.ts exactly.\n */\n\nimport type {\n BooleanSettingConfig,\n JsonSettingConfig,\n MediaSettingConfig,\n NumberSettingConfig,\n SelectSettingConfig,\n TextSettingConfig,\n} from './types.js'\n\n/**\n * Concentrates the generic-spread cast TS can't do on its own — every builder\n * below funnels through this helper, so the codebase has exactly one cast in\n * place of six.\n */\nfunction build<T extends { type: string }, C>(type: T['type'], config?: C): T & C {\n return { type, ...config } as T & C\n}\n\n// biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\ntype Empty = {}\n\nexport const setting = {\n /**\n * Text setting (string value)\n */\n text: <const C extends Partial<Omit<TextSettingConfig, 'type'>> = Empty>(\n config?: C,\n ): TextSettingConfig & C => build<TextSettingConfig, C>('text', config),\n\n /**\n * Number setting\n */\n number: <const C extends Partial<Omit<NumberSettingConfig, 'type'>> = Empty>(\n config?: C,\n ): NumberSettingConfig & C => build<NumberSettingConfig, C>('number', config),\n\n /**\n * Boolean setting\n */\n boolean: <const C extends Partial<Omit<BooleanSettingConfig, 'type'>> = Empty>(\n config?: C,\n ): BooleanSettingConfig & C => build<BooleanSettingConfig, C>('boolean', config),\n\n /**\n * Select setting (enum from options tuple)\n * Preserves literal option types for type inference.\n *\n * @example\n * setting.select({ options: ['light', 'dark', 'system'] as const, default: 'system' })\n * // inferred type: 'light' | 'dark' | 'system'\n */\n select: <\n const O extends readonly string[],\n const C extends Partial<Omit<SelectSettingConfig, 'type' | 'options'>> = Empty,\n >(\n config: { options: O } & C,\n ): SelectSettingConfig<O> & C =>\n build<SelectSettingConfig<O>, { options: O } & C>('select', config),\n\n /**\n * JSON setting (arbitrary typed JSON)\n *\n * @example\n * setting.json<{ twitter?: string; github?: string }>()\n * setting.json<string[]>({ default: [] })\n */\n json: <T = unknown, const C extends Partial<Omit<JsonSettingConfig<T>, 'type'>> = Empty>(\n config?: C,\n ): JsonSettingConfig<T> & C => build<JsonSettingConfig<T>, C>('json', config),\n\n /**\n * Media setting (stores media ID as string UUID)\n */\n media: <const C extends Partial<Omit<MediaSettingConfig, 'type'>> = Empty>(\n config?: C,\n ): MediaSettingConfig & C => build<MediaSettingConfig, C>('media', config),\n}\n","/**\n * Setting configuration types and compile-time type inference.\n *\n * Design mirrors the entity field system:\n * - Config interfaces define what each setting type accepts\n * - SettingToTS maps a single config to its TypeScript type\n * - InferSettingValue adds null awareness based on `default` presence\n * - InferSettingsMap maps an entire schema to a typed record\n */\n\nimport type { ZodType } from 'zod'\n\n// ---------------------------------------------------------------\n// 1. Setting config interfaces\n// ---------------------------------------------------------------\n\nexport interface BaseSettingConfig {\n /** Human-readable label for admin UI */\n label?: string\n /** Description / help text */\n description?: string\n /** If true, this setting can have per-locale values (mirrors entity translatable pattern) */\n translatable?: boolean\n}\n\nexport interface TextSettingConfig extends BaseSettingConfig {\n type: 'text'\n default?: string\n maxLength?: number\n minLength?: number\n pattern?: RegExp\n /** Render as textarea instead of single-line input. */\n multiline?: boolean\n}\n\nexport interface NumberSettingConfig extends BaseSettingConfig {\n type: 'number'\n default?: number\n min?: number\n max?: number\n integer?: boolean\n}\n\nexport interface BooleanSettingConfig extends BaseSettingConfig {\n type: 'boolean'\n default?: boolean\n}\n\nexport interface SelectSettingConfig<O extends readonly string[] = readonly string[]>\n extends BaseSettingConfig {\n type: 'select'\n options: O\n default?: O[number]\n}\n\nexport interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {\n type: 'json'\n default?: T\n /** Optional Zod schema for validation. If provided, values are validated on set. */\n schema?: ZodType<T>\n /**\n * Optional renderer slot key. When set, the settings form looks up\n * `renderers[renderer]` (provided by the admin shell) and renders that\n * component in place of the default JSON textarea. Falls back to the\n * JSON renderer if the slot key is absent from the renderer map.\n *\n * The component receives `{ value, onChange, error }` and is responsible\n * for editing the field's value. Convention: prefix with the namespace\n * (e.g. `'media.imageStyles'`, `'commerce.searchRegistry'`) to avoid\n * collisions across plugins.\n */\n renderer?: string\n}\n\nexport interface MediaSettingConfig extends BaseSettingConfig {\n type: 'media'\n default?: string\n accept?: string[]\n}\n\nexport type SettingConfig =\n | TextSettingConfig\n | NumberSettingConfig\n | BooleanSettingConfig\n | SelectSettingConfig\n | JsonSettingConfig\n | MediaSettingConfig\n\n// ---------------------------------------------------------------\n// 2. Setting-to-TypeScript mapping (single setting)\n// ---------------------------------------------------------------\n\n/**\n * Maps a single SettingConfig to its TypeScript output type.\n * Each branch is a shallow comparison — no recursion.\n */\nexport type SettingToTS<S extends SettingConfig> = S extends TextSettingConfig\n ? string\n : S extends NumberSettingConfig\n ? number\n : S extends BooleanSettingConfig\n ? boolean\n : S extends SelectSettingConfig<infer O>\n ? O[number]\n : S extends JsonSettingConfig<infer T>\n ? T\n : S extends MediaSettingConfig\n ? string\n : never\n\n// ---------------------------------------------------------------\n// 3. Null awareness: settings with defaults always return value\n// ---------------------------------------------------------------\n\n/**\n * If a setting has a `default`, get() never returns null.\n * Without a default, it returns T | null.\n */\nexport type InferSettingValue<S extends SettingConfig> = S extends { default: unknown }\n ? SettingToTS<S>\n : SettingToTS<S> | null\n\n// ---------------------------------------------------------------\n// 4. Full settings map type (what getAll() returns)\n// ---------------------------------------------------------------\n\nexport type InferSettingsMap<Schema extends Record<string, SettingConfig>> = {\n [K in keyof Schema]: InferSettingValue<Schema[K]>\n}\n\n// ---------------------------------------------------------------\n// 5. Scope types\n// ---------------------------------------------------------------\n\nexport type SettingScope = 'global' | 'team' | 'user'\n\n/** Sentinel value for global scope_id (avoids NULL uniqueness issues) */\nexport const GLOBAL_SCOPE_ID = '__global__'\n\n/** Sentinel value for locale column when no locale is specified (base/default value) */\nexport const DEFAULT_LOCALE = '_default'\n\n// ---------------------------------------------------------------\n// 6. Settings definition (returned by defineSettings)\n// ---------------------------------------------------------------\n\n/**\n * Props every custom JSON field renderer (`renderer` slot) receives.\n *\n * `value` and `onChange` plug straight into react-hook-form's Controller;\n * `error` carries the field-level error message (from Zod or server-side\n * field errors) so the renderer can surface inline feedback without\n * needing access to the form context.\n *\n * Plugins that ship a renderer write their component against\n * `SettingFieldRendererProps<TheirValueType>` and contribute it via\n * `Plugin.adminUi.settingRenderers`. The framework dispatches with\n * `value: unknown` at the storage boundary; type narrowing is the\n * plugin author's responsibility (the same author owns the schema, so\n * the value shape is a known contract).\n *\n * Type lives here (not in admin-ui) to keep plugin packages free of an\n * `@murumets-ee/admin-ui` dependency, which would create a cycle.\n */\nexport interface SettingFieldRendererProps<T = unknown> {\n value: T\n onChange: (value: T) => void\n error?: string | undefined\n}\n\nexport interface SettingsDefinition<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n /** Unique namespace for this settings group */\n namespace: string\n /** Default scope for these settings */\n scope: SettingScope\n /** Setting schema (the shape) */\n schema: S\n /** Human-readable label for admin UI */\n label?: string\n /**\n * Lucide icon name (e.g. `'globe'`, `'ticket'`) for the sidebar nav\n * entry this definition produces. Falls back to `'settings'` when\n * unset.\n */\n iconName?: string\n /**\n * Hide this namespace from the auto-generated sidebar nav. Still\n * renders in the settings API. Useful for internal-only namespaces\n * like the permissions store.\n */\n hideFromMenu?: boolean\n}\n","/**\n * Define a typed settings group.\n *\n * @example\n * ```typescript\n * import { defineSettings, setting } from '@murumets-ee/settings'\n *\n * export const siteSettings = defineSettings({\n * namespace: 'site',\n * scope: 'global',\n * schema: {\n * siteName: setting.text({ default: 'My Site' }),\n * logo: setting.media(),\n * maintenance: setting.boolean({ default: false }),\n * },\n * })\n * ```\n */\n\nimport type { SettingConfig, SettingScope, SettingsDefinition } from './types.js'\n\n/**\n * Permitted namespace shape. Must start with a letter, end with a letter or\n * digit, and only contain letters, digits, and the punctuation `_`, `-`,\n * `.`, `:` — never two of those in a row (so `foo..bar`, `foo--bar`, `foo:.bar`\n * are all rejected).\n *\n * Why these characters: existing namespaces use both `:` (e.g.\n * `ticketing:agent`) and `.` (e.g. `media.imageStyles`); both are legal\n * `pchar` URL segments per RFC 3986 and unambiguous in the permission\n * resource string `settings:<namespace>`. What this excludes (and\n * intentionally so): `/` (path separator), whitespace, repeat punctuation\n * (`..` is the canonical path-traversal smell), control chars, and anything\n * that would let a typo'd namespace land at a weird URL or smuggle into a\n * permission resource name.\n */\nconst NAMESPACE_PATTERN = /^[a-zA-Z](?:[a-zA-Z0-9]|[_.:-][a-zA-Z0-9])*$/\n\nexport function defineSettings<const S extends Record<string, SettingConfig>>(definition: {\n namespace: string\n scope: SettingScope\n schema: S\n label?: string\n /** Lucide icon name for the auto-generated sidebar entry. */\n iconName?: string\n /** Hide this namespace from the auto-generated sidebar nav. */\n hideFromMenu?: boolean\n}): SettingsDefinition<S> {\n if (!definition.namespace) {\n throw new Error('Settings namespace is required')\n }\n if (!NAMESPACE_PATTERN.test(definition.namespace)) {\n throw new Error(\n `Settings namespace \"${definition.namespace}\" is invalid: must start with a letter, end with a letter or digit, and use only [a-zA-Z0-9] or single _ . : - separators (no consecutive punctuation).`,\n )\n }\n if (!definition.schema || Object.keys(definition.schema).length === 0) {\n throw new Error('Settings schema must have at least one setting')\n }\n return definition\n}\n"],"mappings":"AAwBA,SAAS,EAAqC,EAAiB,EAAmB,CAChF,MAAO,CAAE,OAAM,GAAG,EAAQ,CAM5B,MAAa,EAAU,CAIrB,KACE,GAC0B,EAA4B,OAAQ,EAAO,CAKvE,OACE,GAC4B,EAA8B,SAAU,EAAO,CAK7E,QACE,GAC6B,EAA+B,UAAW,EAAO,CAUhF,OAIE,GAEA,EAAkD,SAAU,EAAO,CASrE,KACE,GAC6B,EAA+B,OAAQ,EAAO,CAK7E,MACE,GAC2B,EAA6B,QAAS,EAAO,CAC3E,CCmDY,EAAkB,aAGlB,EAAiB,WCxGxB,EAAoB,+CAE1B,SAAgB,EAA8D,EASpD,CACxB,GAAI,CAAC,EAAW,UACd,MAAU,MAAM,iCAAiC,CAEnD,GAAI,CAAC,EAAkB,KAAK,EAAW,UAAU,CAC/C,MAAU,MACR,uBAAuB,EAAW,UAAU,yJAC7C,CAEH,GAAI,CAAC,EAAW,QAAU,OAAO,KAAK,EAAW,OAAO,CAAC,SAAW,EAClE,MAAU,MAAM,iDAAiD,CAEnE,OAAO"}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { c as MediaSettingConfig, d as SettingConfig,
|
|
1
|
+
import { c as MediaSettingConfig, d as SettingConfig, g as TextSettingConfig, h as SettingsDefinition, l as NumberSettingConfig, n as BooleanSettingConfig, p as SettingScope, s as JsonSettingConfig, u as SelectSettingConfig } from "./types-C_SUmm7Q.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/builders.d.ts
|
|
4
|
+
type Empty = {};
|
|
4
5
|
declare const setting: {
|
|
5
6
|
/**
|
|
6
7
|
* Text setting (string value)
|
|
7
8
|
*/
|
|
8
|
-
text: <const C extends Partial<Omit<TextSettingConfig, "type">> =
|
|
9
|
+
text: <const C extends Partial<Omit<TextSettingConfig, "type">> = Empty>(config?: C) => TextSettingConfig & C;
|
|
9
10
|
/**
|
|
10
11
|
* Number setting
|
|
11
12
|
*/
|
|
12
|
-
number: <const C extends Partial<Omit<NumberSettingConfig, "type">> =
|
|
13
|
+
number: <const C extends Partial<Omit<NumberSettingConfig, "type">> = Empty>(config?: C) => NumberSettingConfig & C;
|
|
13
14
|
/**
|
|
14
15
|
* Boolean setting
|
|
15
16
|
*/
|
|
16
|
-
boolean: <const C extends Partial<Omit<BooleanSettingConfig, "type">> =
|
|
17
|
+
boolean: <const C extends Partial<Omit<BooleanSettingConfig, "type">> = Empty>(config?: C) => BooleanSettingConfig & C;
|
|
17
18
|
/**
|
|
18
19
|
* Select setting (enum from options tuple)
|
|
19
20
|
* Preserves literal option types for type inference.
|
|
@@ -22,7 +23,7 @@ declare const setting: {
|
|
|
22
23
|
* setting.select({ options: ['light', 'dark', 'system'] as const, default: 'system' })
|
|
23
24
|
* // inferred type: 'light' | 'dark' | 'system'
|
|
24
25
|
*/
|
|
25
|
-
select: <const O extends readonly string[], const C extends Partial<Omit<SelectSettingConfig, "type" | "options">> =
|
|
26
|
+
select: <const O extends readonly string[], const C extends Partial<Omit<SelectSettingConfig, "type" | "options">> = Empty>(config: {
|
|
26
27
|
options: O;
|
|
27
28
|
} & C) => SelectSettingConfig<O> & C;
|
|
28
29
|
/**
|
|
@@ -32,11 +33,11 @@ declare const setting: {
|
|
|
32
33
|
* setting.json<{ twitter?: string; github?: string }>()
|
|
33
34
|
* setting.json<string[]>({ default: [] })
|
|
34
35
|
*/
|
|
35
|
-
json: <T = unknown, const C extends Partial<Omit<JsonSettingConfig<T>, "type">> =
|
|
36
|
+
json: <T = unknown, const C extends Partial<Omit<JsonSettingConfig<T>, "type">> = Empty>(config?: C) => JsonSettingConfig<T> & C;
|
|
36
37
|
/**
|
|
37
38
|
* Media setting (stores media ID as string UUID)
|
|
38
39
|
*/
|
|
39
|
-
media: <const C extends Partial<Omit<MediaSettingConfig, "type">> =
|
|
40
|
+
media: <const C extends Partial<Omit<MediaSettingConfig, "type">> = Empty>(config?: C) => MediaSettingConfig & C;
|
|
40
41
|
};
|
|
41
42
|
//#endregion
|
|
42
43
|
//#region src/define-settings.d.ts
|
|
@@ -44,8 +45,10 @@ declare function defineSettings<const S extends Record<string, SettingConfig>>(d
|
|
|
44
45
|
namespace: string;
|
|
45
46
|
scope: SettingScope;
|
|
46
47
|
schema: S;
|
|
47
|
-
label?: string;
|
|
48
|
+
label?: string; /** Lucide icon name for the auto-generated sidebar entry. */
|
|
49
|
+
iconName?: string; /** Hide this namespace from the auto-generated sidebar nav. */
|
|
50
|
+
hideFromMenu?: boolean;
|
|
48
51
|
}): SettingsDefinition<S>;
|
|
49
52
|
//#endregion
|
|
50
53
|
export { setting as n, defineSettings as t };
|
|
51
|
-
//# sourceMappingURL=define-settings-
|
|
54
|
+
//# sourceMappingURL=define-settings-DvZ1SBIP.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-settings-DvZ1SBIP.d.mts","names":[],"sources":["../src/builders.ts","../src/define-settings.ts"],"mappings":";;;KA6BK,KAAA;AAAA,cAEQ,OAAA;EAIoB;;;yBAAR,OAAA,CAAQ,IAAA,CAAK,iBAAA,aAA2B,KAAA,EAAA,MAAA,GACpD,CAAA,KACR,iBAAA,GAAoB,CAAA;EAApB;;;2BAKsB,OAAA,CAAQ,IAAA,CAAK,mBAAA,aAA6B,KAAA,EAAA,MAAA,GACxD,CAAA,KACR,mBAAA,GAAsB,CAAA;EAFA;;;4BAOC,OAAA,CAAQ,IAAA,CAAK,oBAAA,aAA8B,KAAA,EAAA,MAAA,GAC1D,CAAA,KACR,oBAAA,GAAuB,CAAA;EAPD;;;;;;;;8DAmBP,OAAA,CAAQ,IAAA,CAAK,mBAAA,yBAAyC,KAAA,EAAA,MAAA;IAE5D,OAAA,EAAS,CAAA;EAAA,IAAM,CAAA,KACxB,mBAAA,CAAoB,CAAA,IAAK,CAAA;EAH4C;;;;;;;sCAapC,OAAA,CAAQ,IAAA,CAAK,iBAAA,CAAkB,CAAA,cAAY,KAAA,EAAA,MAAA,GACpE,CAAA,KACR,iBAAA,CAAkB,CAAA,IAAK,CAAA;EAFkB;;;0BAOpB,OAAA,CAAQ,IAAA,CAAK,kBAAA,aAA4B,KAAA,EAAA,MAAA,GACtD,CAAA,KACR,kBAAA,GAAqB,CAAA;AAAA;;;iBC/CV,cAAA,iBAA+B,MAAA,SAAe,aAAA,EAAA,CAAgB,UAAA;EAC5E,SAAA;EACA,KAAA,EAAO,YAAA;EACP,MAAA,EAAQ,CAAA;EACR,KAAA,WDEyB;ECAzB,QAAA,WDKkC;ECHlC,YAAA;AAAA,IACE,kBAAA,CAAmB,CAAA"}
|
package/dist/define.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as
|
|
2
|
-
import { n as setting, t as defineSettings } from "./define-settings-
|
|
3
|
-
export { type BaseSettingConfig, type BooleanSettingConfig, DEFAULT_LOCALE, GLOBAL_SCOPE_ID, type InferSettingValue, type InferSettingsMap, type JsonSettingConfig, type MediaSettingConfig, type NumberSettingConfig, type SelectSettingConfig, type SettingConfig, type SettingScope, type SettingToTS, type SettingsDefinition, type TextSettingConfig, defineSettings, setting };
|
|
1
|
+
import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as SettingFieldRendererProps, g as TextSettingConfig, h as SettingsDefinition, i as GLOBAL_SCOPE_ID, l as NumberSettingConfig, m as SettingToTS, n as BooleanSettingConfig, o as InferSettingsMap, p as SettingScope, r as DEFAULT_LOCALE, s as JsonSettingConfig, t as BaseSettingConfig, u as SelectSettingConfig } from "./types-C_SUmm7Q.mjs";
|
|
2
|
+
import { n as setting, t as defineSettings } from "./define-settings-DvZ1SBIP.mjs";
|
|
3
|
+
export { type BaseSettingConfig, type BooleanSettingConfig, DEFAULT_LOCALE, GLOBAL_SCOPE_ID, type InferSettingValue, type InferSettingsMap, type JsonSettingConfig, type MediaSettingConfig, type NumberSettingConfig, type SelectSettingConfig, type SettingConfig, type SettingFieldRendererProps, type SettingScope, type SettingToTS, type SettingsDefinition, type TextSettingConfig, defineSettings, setting };
|
package/dist/define.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,n as t,r as n,t as r}from"./define-settings-
|
|
1
|
+
import{i as e,n as t,r as n,t as r}from"./define-settings-DHaHn6XA.mjs";export{t as DEFAULT_LOCALE,n as GLOBAL_SCOPE_ID,r as defineSettings,e as setting};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as
|
|
2
|
-
import { n as setting, t as defineSettings } from "./define-settings-
|
|
3
|
-
import { a as SettingsClientConfig, i as SettingsClient, n as createSettingsClient, r as LocaleOption, t as CreateSettingsClientOptions } from "./client-factory-
|
|
4
|
-
export { type BaseSettingConfig, type BooleanSettingConfig, type CreateSettingsClientOptions, DEFAULT_LOCALE, GLOBAL_SCOPE_ID, type InferSettingValue, type InferSettingsMap, type JsonSettingConfig, type LocaleOption, type MediaSettingConfig, type NumberSettingConfig, type SelectSettingConfig, type SettingConfig, type SettingScope, type SettingToTS, SettingsClient, type SettingsClientConfig, type SettingsDefinition, type TextSettingConfig, createSettingsClient, defineSettings, setting };
|
|
1
|
+
import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as SettingFieldRendererProps, g as TextSettingConfig, h as SettingsDefinition, i as GLOBAL_SCOPE_ID, l as NumberSettingConfig, m as SettingToTS, n as BooleanSettingConfig, o as InferSettingsMap, p as SettingScope, r as DEFAULT_LOCALE, s as JsonSettingConfig, t as BaseSettingConfig, u as SelectSettingConfig } from "./types-C_SUmm7Q.mjs";
|
|
2
|
+
import { n as setting, t as defineSettings } from "./define-settings-DvZ1SBIP.mjs";
|
|
3
|
+
import { a as SettingsClientConfig, i as SettingsClient, n as createSettingsClient, r as LocaleOption, t as CreateSettingsClientOptions } from "./client-factory-BTSJWdrk.mjs";
|
|
4
|
+
export { type BaseSettingConfig, type BooleanSettingConfig, type CreateSettingsClientOptions, DEFAULT_LOCALE, GLOBAL_SCOPE_ID, type InferSettingValue, type InferSettingsMap, type JsonSettingConfig, type LocaleOption, type MediaSettingConfig, type NumberSettingConfig, type SelectSettingConfig, type SettingConfig, type SettingFieldRendererProps, type SettingScope, type SettingToTS, SettingsClient, type SettingsClientConfig, type SettingsDefinition, type TextSettingConfig, createSettingsClient, defineSettings, setting };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,n as t,r as n,t as r}from"./define-settings-
|
|
1
|
+
import{i as e,n as t,r as n,t as r}from"./define-settings-DHaHn6XA.mjs";import{n as i,t as a}from"./client-factory-B8jGDCJn.mjs";import"server-only";export{t as DEFAULT_LOCALE,n as GLOBAL_SCOPE_ID,i as SettingsClient,a as createSettingsClient,r as defineSettings,e as setting};
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as SettingsDefinition } from "./types-C_SUmm7Q.mjs";
|
|
2
2
|
import { Plugin } from "@murumets-ee/core";
|
|
3
3
|
|
|
4
4
|
//#region src/plugin.d.ts
|
|
5
5
|
interface SettingsPluginOptions {
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - one permission resource `settings:<namespace>` (view + update)
|
|
11
|
-
* - one dispatch branch in `settingsRoutes` for `GET|PATCH /api/admin/settings/<namespace>`
|
|
12
|
-
* - one `shared.settings` entry (for namespace-collision detection and
|
|
13
|
-
* downstream admin-page rendering)
|
|
14
|
-
*
|
|
15
|
-
* Must be the FULL list — other plugins cannot contribute settings
|
|
16
|
-
* namespaces independently because `settingsRoutes` has a single
|
|
17
|
-
* `settings` prefix that multiplexes across every namespace.
|
|
7
|
+
* App-defined settings namespaces exposed through the admin settings API.
|
|
8
|
+
* Plugin-contributed namespaces (`Plugin.shared.settings`) are merged in
|
|
9
|
+
* automatically at init time.
|
|
18
10
|
*/
|
|
19
|
-
definitions
|
|
11
|
+
definitions?: SettingsDefinition[];
|
|
20
12
|
}
|
|
21
|
-
declare function settings(options
|
|
13
|
+
declare function settings(options?: SettingsPluginOptions): Plugin;
|
|
22
14
|
//#endregion
|
|
23
15
|
export { SettingsPluginOptions, settings };
|
|
24
16
|
//# sourceMappingURL=plugin.d.mts.map
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;UAqCiB,qBAAA;;;;;;EAMf,WAAA,GAAc,kBAAA;AAAA;AAAA,iBAGA,QAAA,CAAS,OAAA,GAAS,qBAAA,GAA6B,MAAA"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{toolkitSettings as e,toolkitViewState as t}from"./schema.mjs";import{settingsRoutes as n}from"@murumets-ee/settings/admin";function r(r){let{definitions:
|
|
1
|
+
import{toolkitSettings as e,toolkitViewState as t}from"./schema.mjs";import{settingsRoutes as n}from"@murumets-ee/settings/admin";function r(r={}){let{definitions:i=[]}=r,a={current:[]};return{name:`@murumets-ee/settings`,server:{tables:{toolkitSettings:e,toolkitViewState:t},routes:[n(()=>a.current)],init:async n=>{let{schemaRegistry:r}=await import(`@murumets-ee/db`);r.has(`toolkit_settings`)||r.register(`toolkit_settings`,e),r.has(`toolkit_view_state`)||r.register(`toolkit_view_state`,t);let o=[],s=new Set;for(let e of i)s.has(e.namespace)||(s.add(e.namespace),o.push(e));for(let e of n.plugins.all()){if(e.name===`@murumets-ee/settings`)continue;let t=e.shared?.settings;if(t)for(let e of t)s.has(e.namespace)||(s.add(e.namespace),o.push(e))}a.current=o,n.logger.info({namespaces:o.map(e=>e.namespace)},`Settings plugin initialized`)}},shared:{settings:i}}}export{r as settings};
|
|
2
2
|
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Settings plugin — registers the settings Drizzle tables, mounts the\n * settings admin API route, and
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Settings plugin — registers the settings Drizzle tables, mounts the\n * settings admin API route, and lets other plugins contribute settings\n * namespaces.\n *\n * @example\n * ```ts\n * import { defineLumiConfig } from '@murumets-ee/core'\n * import { settings } from '@murumets-ee/settings/plugin'\n * import { siteSettings } from './settings/site'\n *\n * export default defineLumiConfig({\n * plugins: [settings({ definitions: [siteSettings] })],\n * })\n * ```\n *\n * Cross-plugin aggregation: any plugin can ship `shared.settings: [...]`\n * with its own `SettingsDefinition`. The settings plugin's init hook\n * walks `app.plugins.all()` and merges those contributions into the\n * route's dispatch list. Permission resources and sidebar entries are\n * auto-derived by `@murumets-ee/core`'s `resolveShell` — plugin authors\n * declare the namespace once and get the full surface for free.\n *\n * The `definitions` argument here is for **app-defined** namespaces\n * (e.g. `siteSettings`, `permissionSettings`) that don't live inside any\n * plugin. Both sources are merged at init time, deduped by namespace\n * (the merge engine throws on duplicates).\n */\n\nimport type { Plugin, PluginSettingsDefinition } from '@murumets-ee/core'\n// Self-reference the `./admin` subpath (not the internal `./admin/routes.js`)\n// so the plugin bundle externalizes `settingsRoutes` instead of inlining\n// its body. Both subpaths are published; consumers pay for one copy.\nimport { settingsRoutes } from '@murumets-ee/settings/admin'\nimport { toolkitSettings, toolkitViewState } from './schema.js'\nimport type { SettingsDefinition } from './types.js'\n\nexport interface SettingsPluginOptions {\n /**\n * App-defined settings namespaces exposed through the admin settings API.\n * Plugin-contributed namespaces (`Plugin.shared.settings`) are merged in\n * automatically at init time.\n */\n definitions?: SettingsDefinition[]\n}\n\nexport function settings(options: SettingsPluginOptions = {}): Plugin {\n const { definitions = [] } = options\n // Mutable registry — `init` swaps `current` with a fresh array each\n // time it runs. Two-fold reason:\n // 1. The route's lazy nsMap rebuilds when the array reference changes.\n // Mutating in place would keep the same reference and the cached\n // nsMap would go stale on HMR / test re-init.\n // 2. The thunk passed to `settingsRoutes(() => registry.current)`\n // stays valid across reassignment because it reads `current`\n // lazily on each request.\n const registry: { current: SettingsDefinition[] } = { current: [] }\n\n return {\n name: '@murumets-ee/settings',\n server: {\n tables: { toolkitSettings, toolkitViewState },\n routes: [settingsRoutes(() => registry.current)],\n init: async (app) => {\n const { schemaRegistry } = await import('@murumets-ee/db')\n\n if (!schemaRegistry.has('toolkit_settings')) {\n schemaRegistry.register('toolkit_settings', toolkitSettings)\n }\n if (!schemaRegistry.has('toolkit_view_state')) {\n schemaRegistry.register('toolkit_view_state', toolkitViewState)\n }\n\n // Build a fresh array — second + Nth init runs (HMR, test re-init)\n // start from scratch so contributions don't accumulate.\n const next: SettingsDefinition[] = []\n\n // App-defined definitions land first so the duplicate check below\n // surfaces conflicts as \"plugin clobbers app\", not the reverse.\n const seen = new Set<string>()\n for (const def of definitions) {\n if (seen.has(def.namespace)) continue\n seen.add(def.namespace)\n next.push(def)\n }\n\n // Merge in every plugin's `shared.settings`. Duplicates with this\n // plugin's own contribution (`@murumets-ee/settings` shares the\n // same `definitions`) are skipped here; the merge engine in core\n // already throws on duplicate namespaces across plugins.\n for (const plugin of app.plugins.all()) {\n if (plugin.name === '@murumets-ee/settings') continue\n const pluginSettings = plugin.shared?.settings\n if (!pluginSettings) continue\n for (const def of pluginSettings) {\n if (seen.has(def.namespace)) continue\n seen.add(def.namespace)\n // PluginSettingsDefinition is structurally widened from\n // SettingsDefinition; runtime values were full SettingsDefinitions\n // when the plugin contributed them. Cast at this single boundary.\n next.push(def as SettingsDefinition)\n }\n }\n\n // Atomic swap — route's nsMap will rebuild on next request.\n registry.current = next\n\n app.logger.info(\n { namespaces: next.map((d) => d.namespace) },\n 'Settings plugin initialized',\n )\n },\n },\n shared: {\n // Full SettingsDefinition objects are structurally assignable to the\n // narrower `PluginSettingsDefinition` (both expose `namespace: string`).\n // The merge engine reads these to auto-derive `pluginResources` and\n // sidebar entries — see resolveShell in @murumets-ee/core.\n settings: definitions as PluginSettingsDefinition[],\n },\n }\n}\n"],"mappings":"kIA8CA,SAAgB,EAAS,EAAiC,EAAE,CAAU,CACpE,GAAM,CAAE,cAAc,EAAE,EAAK,EASvB,EAA8C,CAAE,QAAS,EAAE,CAAE,CAEnE,MAAO,CACL,KAAM,wBACN,OAAQ,CACN,OAAQ,CAAE,kBAAiB,mBAAkB,CAC7C,OAAQ,CAAC,MAAqB,EAAS,QAAQ,CAAC,CAChD,KAAM,KAAO,IAAQ,CACnB,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAEnC,EAAe,IAAI,mBAAmB,EACzC,EAAe,SAAS,mBAAoB,EAAgB,CAEzD,EAAe,IAAI,qBAAqB,EAC3C,EAAe,SAAS,qBAAsB,EAAiB,CAKjE,IAAM,EAA6B,EAAE,CAI/B,EAAO,IAAI,IACjB,IAAK,IAAM,KAAO,EACZ,EAAK,IAAI,EAAI,UAAU,GAC3B,EAAK,IAAI,EAAI,UAAU,CACvB,EAAK,KAAK,EAAI,EAOhB,IAAK,IAAM,KAAU,EAAI,QAAQ,KAAK,CAAE,CACtC,GAAI,EAAO,OAAS,wBAAyB,SAC7C,IAAM,EAAiB,EAAO,QAAQ,SACjC,KACL,IAAK,IAAM,KAAO,EACZ,EAAK,IAAI,EAAI,UAAU,GAC3B,EAAK,IAAI,EAAI,UAAU,CAIvB,EAAK,KAAK,EAA0B,EAKxC,EAAS,QAAU,EAEnB,EAAI,OAAO,KACT,CAAE,WAAY,EAAK,IAAK,GAAM,EAAE,UAAU,CAAE,CAC5C,8BACD,EAEJ,CACD,OAAQ,CAKN,SAAU,EACX,CACF"}
|
package/dist/runtime.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as setting, t as defineSettings } from "./define-settings-
|
|
2
|
-
import { a as SettingsClientConfig, i as SettingsClient, n as createSettingsClient, r as LocaleOption, t as CreateSettingsClientOptions } from "./client-factory-
|
|
1
|
+
import { n as setting, t as defineSettings } from "./define-settings-DvZ1SBIP.mjs";
|
|
2
|
+
import { a as SettingsClientConfig, i as SettingsClient, n as createSettingsClient, r as LocaleOption, t as CreateSettingsClientOptions } from "./client-factory-BTSJWdrk.mjs";
|
|
3
3
|
export { type CreateSettingsClientOptions, type LocaleOption, SettingsClient, type SettingsClientConfig, createSettingsClient, defineSettings, setting };
|
package/dist/runtime.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,t}from"./define-settings-
|
|
1
|
+
import{i as e,t}from"./define-settings-DHaHn6XA.mjs";import{n,t as r}from"./client-factory-B8jGDCJn.mjs";export{n as SettingsClient,r as createSettingsClient,t as defineSettings,e as setting};
|
package/dist/schema.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _$_murumets_ee_db0 from "@murumets-ee/db";
|
|
2
|
-
import * as _$drizzle_orm0 from "drizzle-orm";
|
|
3
2
|
import * as _$drizzle_orm_postgres_js0 from "drizzle-orm/postgres-js";
|
|
3
|
+
import * as _$drizzle_orm0 from "drizzle-orm";
|
|
4
4
|
import * as _$drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
|
|
5
5
|
|
|
6
6
|
//#region src/schema.d.ts
|
|
@@ -62,7 +62,7 @@ declare const toolkitSettingsTable: {
|
|
|
62
62
|
readonly locale: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, true>;
|
|
63
63
|
readonly value: _$_murumets_ee_db0.ColumnFactory<unknown, "jsonb", false, false>;
|
|
64
64
|
readonly updatedAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", true, true>;
|
|
65
|
-
readonly updatedBy: _$_murumets_ee_db0.ColumnFactory<string, "
|
|
65
|
+
readonly updatedBy: _$_murumets_ee_db0.ColumnFactory<string, "varchar", false, false>;
|
|
66
66
|
}>;
|
|
67
67
|
columnKinds: Readonly<Record<string, _$_murumets_ee_db0.ColumnKind>>;
|
|
68
68
|
primaryKeyColumns: readonly string[];
|
|
@@ -75,7 +75,7 @@ declare const toolkitSettingsTable: {
|
|
|
75
75
|
readonly locale: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, true>;
|
|
76
76
|
readonly value: _$_murumets_ee_db0.ColumnFactory<unknown, "jsonb", false, false>;
|
|
77
77
|
readonly updatedAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", true, true>;
|
|
78
|
-
readonly updatedBy: _$_murumets_ee_db0.ColumnFactory<string, "
|
|
78
|
+
readonly updatedBy: _$_murumets_ee_db0.ColumnFactory<string, "varchar", false, false>;
|
|
79
79
|
}, _$drizzle_orm_pg_core0.PgTableWithColumns<{
|
|
80
80
|
name: string;
|
|
81
81
|
schema: undefined;
|
|
@@ -159,7 +159,7 @@ declare const toolkitViewStateTable: {
|
|
|
159
159
|
}>;
|
|
160
160
|
schema: _$_murumets_ee_db0.TableDefinition<{
|
|
161
161
|
readonly id: _$_murumets_ee_db0.ColumnFactory<string, "uuid", true, true>;
|
|
162
|
-
readonly userId: _$_murumets_ee_db0.ColumnFactory<string, "
|
|
162
|
+
readonly userId: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
|
|
163
163
|
readonly viewKey: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
|
|
164
164
|
readonly state: _$_murumets_ee_db0.ColumnFactory<unknown, "jsonb", true, false>;
|
|
165
165
|
readonly expiresAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", false, false>;
|
|
@@ -169,7 +169,7 @@ declare const toolkitViewStateTable: {
|
|
|
169
169
|
primaryKeyColumns: readonly string[];
|
|
170
170
|
makeClient: (db: _$drizzle_orm_postgres_js0.PostgresJsDatabase) => _$_murumets_ee_db0.TableClient<{
|
|
171
171
|
readonly id: _$_murumets_ee_db0.ColumnFactory<string, "uuid", true, true>;
|
|
172
|
-
readonly userId: _$_murumets_ee_db0.ColumnFactory<string, "
|
|
172
|
+
readonly userId: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
|
|
173
173
|
readonly viewKey: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
|
|
174
174
|
readonly state: _$_murumets_ee_db0.ColumnFactory<unknown, "jsonb", true, false>;
|
|
175
175
|
readonly expiresAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", false, false>;
|
package/dist/schema.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.mts","names":[],"sources":["../src/schema.ts"],"mappings":";;;;;;;;;;;;AA2BA;;;;;;;;;;;;;;;;;;cAAa,oBAAA;;;;;;;;
|
|
1
|
+
{"version":3,"file":"schema.d.mts","names":[],"sources":["../src/schema.ts"],"mappings":";;;;;;;;;;;;AA2BA;;;;;;;;;;;;;;;;;;cAAa,oBAAA;;;;;;;;kBAwBX,cAAA,CAAA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAGW,eAAA,yBAAe,kBAAA;;;;;;;gBAA6B,cAAA,CAAA,cAAA;;;;;;;;;;;;;;;;;;;;;;;cAQ5C,qBAAA;;;;;;;;kBAgBX,cAAA,CAAA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAGW,gBAAA,yBAAgB,kBAAA;;;;;;;gBAA8B,cAAA,CAAA,cAAA"}
|
package/dist/schema.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{column as e,defineTable as t}from"@murumets-ee/db";const n=t({name:`toolkit_settings`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),namespace:e.varchar({length:100,notNull:!0}),scope:e.varchar({length:20,notNull:!0,default:`global`}),scopeId:e.varchar({length:100,notNull:!0,default:`__global__`,pgName:`scope_id`}),key:e.varchar({length:255,notNull:!0}),locale:e.varchar({length:10,notNull:!0,default:`_default`}),value:e.jsonb(),updatedAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`}),updatedBy:e.
|
|
1
|
+
import{column as e,defineTable as t}from"@murumets-ee/db";const n=t({name:`toolkit_settings`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),namespace:e.varchar({length:100,notNull:!0}),scope:e.varchar({length:20,notNull:!0,default:`global`}),scopeId:e.varchar({length:100,notNull:!0,default:`__global__`,pgName:`scope_id`}),key:e.varchar({length:255,notNull:!0}),locale:e.varchar({length:10,notNull:!0,default:`_default`}),value:e.jsonb(),updatedAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`}),updatedBy:e.varchar({length:255,pgName:`updated_by`})},unique:[{on:[`namespace`,`scope`,`scopeId`,`key`,`locale`]}]}),r=n.table,i=t({name:`toolkit_view_state`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),userId:e.varchar({length:255,notNull:!0,pgName:`user_id`}),viewKey:e.varchar({length:255,notNull:!0,pgName:`view_key`}),state:e.jsonb({notNull:!0}),expiresAt:e.timestamp({withTimezone:!0,pgName:`expires_at`}),updatedAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`userId`,`viewKey`]}]}),a=i.table;export{r as toolkitSettings,n as toolkitSettingsTable,a as toolkitViewState,i as toolkitViewStateTable};
|
|
2
2
|
//# sourceMappingURL=schema.mjs.map
|
package/dist/schema.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.mjs","names":[],"sources":["../src/schema.ts"],"sourcesContent":["/**\n * Drizzle schema for settings tables.\n *\n * Two tables:\n * - toolkit_settings: typed key-value settings grouped by namespace and scope\n * - toolkit_view_state: schemaless user-scoped JSON blobs with optional TTL\n *\n * These tables are registered for migration discovery via the `settings()`\n * plugin's `tables` field. `lumi migrate` picks them up automatically.\n *\n * Built on `@murumets-ee/db`'s `defineTable` — the raw `pgTable` is still\n * re-exported (via `.table`) for backwards compatibility with existing\n * imports and tests, but all query construction in SettingsClient /\n * ViewStateClient goes through the typed TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\n/**\n * Typed settings table.\n *\n * Stores key-value pairs grouped by namespace and scoped\n * to global, team, or user contexts.\n *\n * scopeId uses '__global__' sentinel for global scope to avoid\n * PostgreSQL's NULL != NULL behavior in unique constraints.\n */\nexport const toolkitSettingsTable = defineTable({\n name: 'toolkit_settings',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n namespace: column.varchar({ length: 100, notNull: true }),\n scope: column.varchar({ length: 20, notNull: true, default: 'global' }),\n scopeId: column.varchar({\n length: 100,\n notNull: true,\n default: '__global__',\n pgName: 'scope_id',\n }),\n key: column.varchar({ length: 255, notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_default' }),\n value: column.jsonb(),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n updatedBy: column.
|
|
1
|
+
{"version":3,"file":"schema.mjs","names":[],"sources":["../src/schema.ts"],"sourcesContent":["/**\n * Drizzle schema for settings tables.\n *\n * Two tables:\n * - toolkit_settings: typed key-value settings grouped by namespace and scope\n * - toolkit_view_state: schemaless user-scoped JSON blobs with optional TTL\n *\n * These tables are registered for migration discovery via the `settings()`\n * plugin's `tables` field. `lumi migrate` picks them up automatically.\n *\n * Built on `@murumets-ee/db`'s `defineTable` — the raw `pgTable` is still\n * re-exported (via `.table`) for backwards compatibility with existing\n * imports and tests, but all query construction in SettingsClient /\n * ViewStateClient goes through the typed TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\n/**\n * Typed settings table.\n *\n * Stores key-value pairs grouped by namespace and scoped\n * to global, team, or user contexts.\n *\n * scopeId uses '__global__' sentinel for global scope to avoid\n * PostgreSQL's NULL != NULL behavior in unique constraints.\n */\nexport const toolkitSettingsTable = defineTable({\n name: 'toolkit_settings',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n namespace: column.varchar({ length: 100, notNull: true }),\n scope: column.varchar({ length: 20, notNull: true, default: 'global' }),\n scopeId: column.varchar({\n length: 100,\n notNull: true,\n default: '__global__',\n pgName: 'scope_id',\n }),\n key: column.varchar({ length: 255, notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_default' }),\n value: column.jsonb(),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n updatedBy: column.varchar({ length: 255, pgName: 'updated_by' }),\n },\n unique: [{ on: ['namespace', 'scope', 'scopeId', 'key', 'locale'] }],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitSettings = toolkitSettingsTable.table\n\n/**\n * View state table.\n *\n * Stores schemaless user-scoped JSON blobs for persisting\n * UI state (table filters, column order, etc.) with optional TTL.\n */\nexport const toolkitViewStateTable = defineTable({\n name: 'toolkit_view_state',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n userId: column.varchar({ length: 255, notNull: true, pgName: 'user_id' }),\n viewKey: column.varchar({ length: 255, notNull: true, pgName: 'view_key' }),\n state: column.jsonb({ notNull: true }),\n expiresAt: column.timestamp({ withTimezone: true, pgName: 'expires_at' }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['userId', 'viewKey'] }],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitViewState = toolkitViewStateTable.table\n"],"mappings":"0DA2BA,MAAa,EAAuB,EAAY,CAC9C,KAAM,mBACN,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,UAAW,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,CAAC,CACzD,MAAO,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,SAAU,CAAC,CACvE,QAAS,EAAO,QAAQ,CACtB,OAAQ,IACR,QAAS,GACT,QAAS,aACT,OAAQ,WACT,CAAC,CACF,IAAK,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,CAAC,CACnD,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,WAAY,CAAC,CAC1E,MAAO,EAAO,OAAO,CACrB,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,aACT,CAAC,CACF,UAAW,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,aAAc,CAAC,CACjE,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,YAAa,QAAS,UAAW,MAAO,SAAS,CAAE,CAAC,CACrE,CAAC,CAGW,EAAkB,EAAqB,MAQvC,EAAwB,EAAY,CAC/C,KAAM,qBACN,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,OAAQ,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,UAAW,CAAC,CACzE,QAAS,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,WAAY,CAAC,CAC3E,MAAO,EAAO,MAAM,CAAE,QAAS,GAAM,CAAC,CACtC,UAAW,EAAO,UAAU,CAAE,aAAc,GAAM,OAAQ,aAAc,CAAC,CACzE,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,aACT,CAAC,CACH,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,SAAU,UAAU,CAAE,CAAC,CACxC,CAAC,CAGW,EAAmB,EAAsB"}
|
|
@@ -39,6 +39,18 @@ interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {
|
|
|
39
39
|
default?: T;
|
|
40
40
|
/** Optional Zod schema for validation. If provided, values are validated on set. */
|
|
41
41
|
schema?: ZodType<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Optional renderer slot key. When set, the settings form looks up
|
|
44
|
+
* `renderers[renderer]` (provided by the admin shell) and renders that
|
|
45
|
+
* component in place of the default JSON textarea. Falls back to the
|
|
46
|
+
* JSON renderer if the slot key is absent from the renderer map.
|
|
47
|
+
*
|
|
48
|
+
* The component receives `{ value, onChange, error }` and is responsible
|
|
49
|
+
* for editing the field's value. Convention: prefix with the namespace
|
|
50
|
+
* (e.g. `'media.imageStyles'`, `'commerce.searchRegistry'`) to avoid
|
|
51
|
+
* collisions across plugins.
|
|
52
|
+
*/
|
|
53
|
+
renderer?: string;
|
|
42
54
|
}
|
|
43
55
|
interface MediaSettingConfig extends BaseSettingConfig {
|
|
44
56
|
type: 'media';
|
|
@@ -64,6 +76,29 @@ type SettingScope = 'global' | 'team' | 'user';
|
|
|
64
76
|
declare const GLOBAL_SCOPE_ID = "__global__";
|
|
65
77
|
/** Sentinel value for locale column when no locale is specified (base/default value) */
|
|
66
78
|
declare const DEFAULT_LOCALE = "_default";
|
|
79
|
+
/**
|
|
80
|
+
* Props every custom JSON field renderer (`renderer` slot) receives.
|
|
81
|
+
*
|
|
82
|
+
* `value` and `onChange` plug straight into react-hook-form's Controller;
|
|
83
|
+
* `error` carries the field-level error message (from Zod or server-side
|
|
84
|
+
* field errors) so the renderer can surface inline feedback without
|
|
85
|
+
* needing access to the form context.
|
|
86
|
+
*
|
|
87
|
+
* Plugins that ship a renderer write their component against
|
|
88
|
+
* `SettingFieldRendererProps<TheirValueType>` and contribute it via
|
|
89
|
+
* `Plugin.adminUi.settingRenderers`. The framework dispatches with
|
|
90
|
+
* `value: unknown` at the storage boundary; type narrowing is the
|
|
91
|
+
* plugin author's responsibility (the same author owns the schema, so
|
|
92
|
+
* the value shape is a known contract).
|
|
93
|
+
*
|
|
94
|
+
* Type lives here (not in admin-ui) to keep plugin packages free of an
|
|
95
|
+
* `@murumets-ee/admin-ui` dependency, which would create a cycle.
|
|
96
|
+
*/
|
|
97
|
+
interface SettingFieldRendererProps<T = unknown> {
|
|
98
|
+
value: T;
|
|
99
|
+
onChange: (value: T) => void;
|
|
100
|
+
error?: string | undefined;
|
|
101
|
+
}
|
|
67
102
|
interface SettingsDefinition<S extends Record<string, SettingConfig> = Record<string, SettingConfig>> {
|
|
68
103
|
/** Unique namespace for this settings group */
|
|
69
104
|
namespace: string;
|
|
@@ -87,5 +122,5 @@ interface SettingsDefinition<S extends Record<string, SettingConfig> = Record<st
|
|
|
87
122
|
hideFromMenu?: boolean;
|
|
88
123
|
}
|
|
89
124
|
//#endregion
|
|
90
|
-
export { InferSettingValue as a, MediaSettingConfig as c, SettingConfig as d,
|
|
91
|
-
//# sourceMappingURL=types-
|
|
125
|
+
export { InferSettingValue as a, MediaSettingConfig as c, SettingConfig as d, SettingFieldRendererProps as f, TextSettingConfig as g, SettingsDefinition as h, GLOBAL_SCOPE_ID as i, NumberSettingConfig as l, SettingToTS as m, BooleanSettingConfig as n, InferSettingsMap as o, SettingScope as p, DEFAULT_LOCALE as r, JsonSettingConfig as s, BaseSettingConfig as t, SelectSettingConfig as u };
|
|
126
|
+
//# sourceMappingURL=types-C_SUmm7Q.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-C_SUmm7Q.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAgBiB,iBAAA;EAMH;EAJZ,KAAA;EAOiC;EALjC,WAAA;EAK0D;EAH1D,YAAA;AAAA;AAAA,UAGe,iBAAA,SAA0B,iBAAA;EACzC,IAAA;EACA,OAAA;EACA,SAAA;EACA,SAAA;EACA,OAAA,GAAU,MAAA;EAED;EAAT,SAAA;AAAA;AAAA,UAGe,mBAAA,SAA4B,iBAAA;EAC3C,IAAA;EACA,OAAA;EACA,GAAA;EACA,GAAA;EACA,OAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,iBAAA;EAC5C,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,mBAAA,0DACP,iBAAA;EACR,IAAA;EACA,OAAA,EAAS,CAAA;EACT,OAAA,GAAU,CAAA;AAAA;AAAA,UAGK,iBAAA,sBAAuC,iBAAA;EACtD,IAAA;EACA,OAAA,GAAU,CAAA;EAZH;EAcP,MAAA,GAAS,OAAA,CAAQ,CAAA;EAXiB;;;;;;;;;;;EAuBlC,QAAA;AAAA;AAAA,UAGe,kBAAA,SAA2B,iBAAA;EAC1C,IAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,KAGU,aAAA,GACR,iBAAA,GACA,mBAAA,GACA,oBAAA,GACA,mBAAA,GACA,iBAAA,GACA,kBAAA;;;;;KAUQ,WAAA,WAAsB,aAAA,IAAiB,CAAA,SAAU,iBAAA,YAEzD,CAAA,SAAU,mBAAA,YAER,CAAA,SAAU,oBAAA,aAER,CAAA,SAAU,mBAAA,YACR,CAAA,WACA,CAAA,SAAU,iBAAA,YACR,CAAA,GACA,CAAA,SAAU,kBAAA;;;;;KAYV,iBAAA,WAA4B,aAAA,IAAiB,CAAA;EAAY,OAAA;AAAA,IACjE,WAAA,CAAY,CAAA,IACZ,WAAA,CAAY,CAAA;AAAA,KAMJ,gBAAA,gBAAgC,MAAA,SAAe,aAAA,mBAC7C,MAAA,GAAS,iBAAA,CAAkB,MAAA,CAAO,CAAA;AAAA,KAOpC,YAAA;;cAGC,eAAA;;cAGA,cAAA;;;;;;;;AA5Db;;;;;;;;;;;UAoFiB,yBAAA;EACf,KAAA,EAAO,CAAA;EACP,QAAA,GAAW,KAAA,EAAO,CAAA;EAClB,KAAA;AAAA;AAAA,UAGe,kBAAA,WACL,MAAA,SAAe,aAAA,IAAiB,MAAA,SAAe,aAAA;EArFrC;EAwFpB,SAAA;EA9EU;EAgFV,KAAA,EAAO,YAAA;EAhFc;EAkFrB,MAAA,EAAQ,CAAA;EAlFyC;EAoFjD,KAAA;EAlFE;;;;;EAwFF,QAAA;EAnFQ;;;;;EAyFR,YAAA;AAAA"}
|
package/dist/view-state.d.mts
CHANGED
|
@@ -8,12 +8,12 @@ interface ViewStateClientConfig {
|
|
|
8
8
|
/** User ID for scoping */
|
|
9
9
|
userId: string;
|
|
10
10
|
/** Logger instance */
|
|
11
|
-
logger?: Logger;
|
|
11
|
+
logger?: Logger | undefined;
|
|
12
12
|
/** Default TTL in seconds for view state entries. Defaults to 30 days. */
|
|
13
|
-
defaultTtl?: number;
|
|
13
|
+
defaultTtl?: number | undefined;
|
|
14
14
|
}
|
|
15
15
|
declare class ViewStateClient {
|
|
16
|
-
private
|
|
16
|
+
private client;
|
|
17
17
|
private userId;
|
|
18
18
|
private logger?;
|
|
19
19
|
private defaultTtl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-state.d.mts","names":[],"sources":["../src/view-state.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"view-state.d.mts","names":[],"sources":["../src/view-state.ts"],"mappings":";;;;UA4BiB,qBAAA;EA8DgC;EA5D/C,EAAA,EAAI,kBAAA;EA8DD;EA5DH,MAAA;EAyFsB;EAvFtB,MAAA,GAAS,MAAA;EAuFoB;EArF7B,UAAA;AAAA;AAAA,cAMW,eAAA;EAAA,QACH,MAAA;EAAA,QACA,MAAA;EAAA,QACA,MAAA;EAAA,QACA,UAAA;cAEI,MAAA,EAAQ,qBAAA;EAcT;;;EAAL,IAAA,WAAe,MAAA,kBAAA,CACnB,OAAA,UACA,KAAA,EAAO,CAAA,EACP,OAAA;IAAY,GAAA;EAAA,IACX,OAAA;EADD;;;EAyBI,IAAA,WAAe,MAAA,oBAA0B,MAAA,kBAAA,CAC7C,OAAA,WACC,OAAA,CAAQ,CAAA;EAFU;;;EAoBf,KAAA,CAAM,OAAA,WAAkB,OAAA;EAlBnB;;;;;EA6BL,YAAA,CAAA,GAAgB,OAAA;AAAA;;AAaxB;;iBAAgB,qBAAA,CAAsB,MAAA,EAAQ,qBAAA,GAAwB,eAAA"}
|
package/dist/view-state.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{toolkitViewStateTable as e}from"./schema.mjs";var t=class{client;userId;logger;defaultTtl;constructor(t){if(typeof window<`u`)throw Error(`ViewStateClient cannot be used in browser code.`);this.client=e.makeClient(t.db),this.userId=t.userId,this.logger=t.logger,this.defaultTtl=t.defaultTtl??2592e3}async save(e,t,n){this.logger?.debug({userId:this.userId,viewKey:e},`Saving view state`);let r=n?.ttl??this.defaultTtl,i=new Date(Date.now()+r*1e3);await this.client.upsert({userId:this.userId,viewKey:e,state:t,expiresAt:i,updatedAt:new Date},{target:[`userId`,`viewKey`],set:{state:t,expiresAt:i,updatedAt:new Date}})}async load(e){this.logger?.debug({userId:this.userId,viewKey:e},`Loading view state`);let t=await this.client.findOne({userId:this.userId,viewKey:e});return t?t.expiresAt&&t.expiresAt<new Date?(await this.clear(e),null):t.state:null}async clear(e){this.logger?.debug({userId:this.userId,viewKey:e},`Clearing view state`),await this.client.delete({userId:this.userId,viewKey:e})}async clearExpired(){this.logger?.info(`Clearing expired view state entries`);let e=(await this.client.deleteMany({expiresAt:{lt:new Date}})).length;return this.logger?.info({count:e},`Expired view state entries cleared`),e}};function n(e){return new t(e)}export{t as ViewStateClient,n as createViewStateClient};
|
|
2
2
|
//# sourceMappingURL=view-state.mjs.map
|
package/dist/view-state.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-state.mjs","names":[],"sources":["../src/view-state.ts"],"sourcesContent":["/**\n * ViewStateClient — schemaless user-scoped state persistence.\n *\n * Used by admin UI for table filters, column order, panel state, etc.\n * Optional TTL for auto-cleanup of stale state.\n *\n * @example\n * ```typescript\n * import { createViewStateClient } from '@murumets-ee/settings/view-state'\n *\n * const viewState = createViewStateClient({ db, userId: currentUser.id })\n *\n * await viewState.save('articles-table', {\n * filters: { status: 'published' },\n * columnOrder: ['title', 'status', 'date'],\n * sortBy: 'date',\n * })\n *\n * const state = await viewState.load('articles-table')\n * ```\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport
|
|
1
|
+
{"version":3,"file":"view-state.mjs","names":[],"sources":["../src/view-state.ts"],"sourcesContent":["/**\n * ViewStateClient — schemaless user-scoped state persistence.\n *\n * Used by admin UI for table filters, column order, panel state, etc.\n * Optional TTL for auto-cleanup of stale state.\n *\n * @example\n * ```typescript\n * import { createViewStateClient } from '@murumets-ee/settings/view-state'\n *\n * const viewState = createViewStateClient({ db, userId: currentUser.id })\n *\n * await viewState.save('articles-table', {\n * filters: { status: 'published' },\n * columnOrder: ['title', 'status', 'date'],\n * sortBy: 'date',\n * })\n *\n * const state = await viewState.load('articles-table')\n * ```\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { toolkitViewStateTable } from './schema.js'\n\ntype ViewStateClient_ = ReturnType<typeof toolkitViewStateTable.makeClient>\n\nexport interface ViewStateClientConfig {\n /** Database client (read-write) */\n db: PostgresJsDatabase\n /** User ID for scoping */\n userId: string\n /** Logger instance */\n logger?: Logger | undefined\n /** Default TTL in seconds for view state entries. Defaults to 30 days. */\n defaultTtl?: number | undefined\n}\n\n/** 30 days in seconds */\nconst DEFAULT_TTL = 30 * 24 * 60 * 60\n\nexport class ViewStateClient {\n private client: ViewStateClient_\n private userId: string\n private logger?: Logger | undefined\n private defaultTtl: number\n\n constructor(config: ViewStateClientConfig) {\n if (typeof window !== 'undefined') {\n throw new Error('ViewStateClient cannot be used in browser code.')\n }\n\n this.client = toolkitViewStateTable.makeClient(config.db)\n this.userId = config.userId\n this.logger = config.logger\n this.defaultTtl = config.defaultTtl ?? DEFAULT_TTL\n }\n\n /**\n * Save view state (upsert).\n */\n async save<T extends Record<string, unknown>>(\n viewKey: string,\n state: T,\n options?: { ttl?: number },\n ): Promise<void> {\n this.logger?.debug({ userId: this.userId, viewKey }, 'Saving view state')\n\n const ttlSeconds = options?.ttl ?? this.defaultTtl\n const expiresAt = new Date(Date.now() + ttlSeconds * 1000)\n\n await this.client.upsert(\n {\n userId: this.userId,\n viewKey,\n state,\n expiresAt,\n updatedAt: new Date(),\n },\n {\n target: ['userId', 'viewKey'],\n set: { state, expiresAt, updatedAt: new Date() },\n },\n )\n }\n\n /**\n * Load view state. Returns null if not found or expired.\n */\n async load<T extends Record<string, unknown> = Record<string, unknown>>(\n viewKey: string,\n ): Promise<T | null> {\n this.logger?.debug({ userId: this.userId, viewKey }, 'Loading view state')\n\n const row = await this.client.findOne({ userId: this.userId, viewKey })\n if (!row) return null\n\n // Check if expired\n if (row.expiresAt && row.expiresAt < new Date()) {\n await this.clear(viewKey)\n return null\n }\n\n return row.state as T\n }\n\n /**\n * Clear a specific view state entry.\n */\n async clear(viewKey: string): Promise<void> {\n this.logger?.debug({ userId: this.userId, viewKey }, 'Clearing view state')\n\n await this.client.delete({ userId: this.userId, viewKey })\n }\n\n /**\n * Clear all expired view state entries (maintenance task).\n * Call periodically (e.g., from a cron job or queue task).\n * Returns the number of entries removed.\n */\n async clearExpired(): Promise<number> {\n this.logger?.info('Clearing expired view state entries')\n\n const removed = await this.client.deleteMany({ expiresAt: { lt: new Date() } })\n const count = removed.length\n this.logger?.info({ count }, 'Expired view state entries cleared')\n return count\n }\n}\n\n/**\n * Factory function following toolkit conventions.\n */\nexport function createViewStateClient(config: ViewStateClientConfig): ViewStateClient {\n return new ViewStateClient(config)\n}\n"],"mappings":"qDA0CA,IAAa,EAAb,KAA6B,CAC3B,OACA,OACA,OACA,WAEA,YAAY,EAA+B,CACzC,GAAI,OAAO,OAAW,IACpB,MAAU,MAAM,kDAAkD,CAGpE,KAAK,OAAS,EAAsB,WAAW,EAAO,GAAG,CACzD,KAAK,OAAS,EAAO,OACrB,KAAK,OAAS,EAAO,OACrB,KAAK,WAAa,EAAO,YAAc,OAMzC,MAAM,KACJ,EACA,EACA,EACe,CACf,KAAK,QAAQ,MAAM,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAE,oBAAoB,CAEzE,IAAM,EAAa,GAAS,KAAO,KAAK,WAClC,EAAY,IAAI,KAAK,KAAK,KAAK,CAAG,EAAa,IAAK,CAE1D,MAAM,KAAK,OAAO,OAChB,CACE,OAAQ,KAAK,OACb,UACA,QACA,YACA,UAAW,IAAI,KAChB,CACD,CACE,OAAQ,CAAC,SAAU,UAAU,CAC7B,IAAK,CAAE,QAAO,YAAW,UAAW,IAAI,KAAQ,CACjD,CACF,CAMH,MAAM,KACJ,EACmB,CACnB,KAAK,QAAQ,MAAM,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAE,qBAAqB,CAE1E,IAAM,EAAM,MAAM,KAAK,OAAO,QAAQ,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAC,CASvE,OARK,EAGD,EAAI,WAAa,EAAI,UAAY,IAAI,MACvC,MAAM,KAAK,MAAM,EAAQ,CAClB,MAGF,EAAI,MARM,KAcnB,MAAM,MAAM,EAAgC,CAC1C,KAAK,QAAQ,MAAM,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAE,sBAAsB,CAE3E,MAAM,KAAK,OAAO,OAAO,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAC,CAQ5D,MAAM,cAAgC,CACpC,KAAK,QAAQ,KAAK,sCAAsC,CAGxD,IAAM,GAAQ,MADQ,KAAK,OAAO,WAAW,CAAE,UAAW,CAAE,GAAI,IAAI,KAAQ,CAAE,CAAC,EACzD,OAEtB,OADA,KAAK,QAAQ,KAAK,CAAE,QAAO,CAAE,qCAAqC,CAC3D,IAOX,SAAgB,EAAsB,EAAgD,CACpF,OAAO,IAAI,EAAgB,EAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/settings",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"drizzle-orm": "^0.45.2",
|
|
41
41
|
"zod": "^3.24.1",
|
|
42
42
|
"server-only": "^0.0.1",
|
|
43
|
-
"@murumets-ee/core": "0.
|
|
44
|
-
"@murumets-ee/db": "0.
|
|
45
|
-
"@murumets-ee/logging": "0.
|
|
43
|
+
"@murumets-ee/core": "0.13.0",
|
|
44
|
+
"@murumets-ee/db": "0.13.0",
|
|
45
|
+
"@murumets-ee/logging": "0.13.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^
|
|
49
|
-
"tsdown": "^0.21.
|
|
48
|
+
"@types/node": "^20.19.39",
|
|
49
|
+
"tsdown": "^0.21.10",
|
|
50
50
|
"typescript": "^5.7.3",
|
|
51
51
|
"vitest": "^2.1.8"
|
|
52
52
|
},
|
|
53
53
|
"typeCoverage": {
|
|
54
|
-
"atLeast":
|
|
54
|
+
"atLeast": 100
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsdown",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{n as e}from"./define-settings-Bi3STtAH.mjs";import{toolkitSettingsTable as t}from"./schema.mjs";import{z as n}from"zod";import{getApp as r}from"@murumets-ee/core";function i(e){switch(e.type){case`text`:{let t=n.string();return e.maxLength&&(t=t.max(e.maxLength)),e.minLength&&(t=t.min(e.minLength)),e.pattern&&(t=t.regex(e.pattern)),t}case`number`:{let t=n.number();return e.integer&&(t=t.int()),e.min!==void 0&&(t=t.min(e.min)),e.max!==void 0&&(t=t.max(e.max)),t}case`boolean`:return n.boolean();case`select`:return n.enum(e.options);case`json`:return e.schema??n.unknown();case`media`:return n.string().uuid();default:return n.unknown()}}function a(e){let t={};for(let[n,r]of Object.entries(e.schema)){let e=i(r);`default`in r&&r.default!==void 0||(e=e.nullable()),t[n]=e}return t}var o=class{definition;table;logger;scope;scopeId;validators;constructor(e,n){if(typeof window<`u`)throw Error(`SettingsClient cannot be used in browser code.`);if(this.definition=e,this.table=t.makeClient(n.db),this.logger=n.logger,this.scope=n.scope??e.scope,this.scopeId=n.scopeId??`__global__`,(this.scope===`team`||this.scope===`user`)&&!n.scopeId)throw Error(`scopeId is required for ${this.scope}-scoped settings (namespace: ${e.namespace})`);this.validators=a(e)}async get(t,n){let r=this.definition.schema[t],i=r?.translatable&&n?.locale?n.locale:null;if(this.logger?.debug({namespace:this.definition.namespace,key:t,locale:i},`Getting setting`),i){let n=await this.table.findMany({where:{...this.baseWhere(),key:t,$or:[{locale:i},{locale:e}]},limit:2}),r=n.find(e=>e.locale===i),a=n.find(t=>t.locale===e),o=r??a;if(o&&o.value!==void 0&&o.value!==null)return o.value}else{let n=await this.table.findMany({where:{...this.baseWhere(),key:t,locale:e},limit:1});if(n.length>0&&n[0].value!==void 0&&n[0].value!==null)return n[0].value}return r&&`default`in r&&r.default!==void 0?r.default:null}async getAll(t){let n=t?.locale??null;this.logger?.debug({namespace:this.definition.namespace,locale:n},`Getting all settings`);let r;r=n?await this.table.findMany({where:{...this.baseWhere(),$or:[{locale:e},{locale:n}]},limit:1e3}):await this.table.findMany({where:{...this.baseWhere(),locale:e},limit:1e3});let i=new Map;for(let e of r){let t=i.get(e.key)??{};e.locale===`_default`?t.default=e.value:t.locale=e.value,i.set(e.key,t)}let a={};for(let[e,t]of Object.entries(this.definition.schema)){let r=i.get(e),o;t.translatable&&n&&r?.locale!==void 0&&r?.locale!==null?o=r.locale:r?.default!==void 0&&r?.default!==null&&(o=r.default),o===void 0?`default`in t&&t.default!==void 0?a[e]=t.default:a[e]=null:a[e]=o}return a}async set(e,t,n){let r=this.resolveLocale(e,n);if(this.logger?.info({namespace:this.definition.namespace,key:e,locale:r},`Setting value`),!(e in this.definition.schema))throw Error(`Unknown setting key '${e}' in namespace '${this.definition.namespace}'`);let i=this.validators[e];i&&i.parse(t),await this.upsertRow(e,t,r,this.table)}async setMany(e,t){this.logger?.info({namespace:this.definition.namespace,keys:Object.keys(e),locale:t?.locale},`Setting multiple values`);for(let[t,n]of Object.entries(e)){if(!(t in this.definition.schema))throw Error(`Unknown setting key '${t}' in namespace '${this.definition.namespace}'`);let e=this.validators[t];e&&n!==void 0&&e.parse(n)}await this.table.transaction(async n=>{for(let[r,i]of Object.entries(e)){if(i===void 0)continue;let e=this.resolveLocale(r,t);await this.upsertRow(r,i,e,n)}})}async delete(e,t){let n=this.resolveLocale(e,t);this.logger?.info({namespace:this.definition.namespace,key:e,locale:n},`Deleting setting`),await this.table.deleteMany({...this.baseWhere(),key:e,locale:n})}async has(e,t){let n=this.resolveLocale(e,t);return await this.table.exists({...this.baseWhere(),key:e,locale:n})}resolveLocale(t,n){return this.definition.schema[t]?.translatable&&n?.locale?n.locale:e}baseWhere(){return{namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId}}async upsertRow(e,t,n,r){await r.upsert({namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId,key:e,locale:n,value:t,updatedAt:new Date},{target:[`namespace`,`scope`,`scopeId`,`key`,`locale`],set:{value:t,updatedAt:new Date}})}};function s(e,t){let n=t?.app??r();return new o(e,{db:n.db.readWrite,logger:n.logger.child({settings:e.namespace}),scope:t?.scope,scopeId:t?.scopeId})}export{o as n,s as t};
|
|
2
|
-
//# sourceMappingURL=client-factory-BLDPF2zz.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-factory-BLDPF2zz.mjs","names":[],"sources":["../src/validation.ts","../src/client.ts","../src/client-factory.ts"],"sourcesContent":["/**\n * Generate Zod validation schemas from setting config definitions.\n * Validates values before writing to the database.\n */\n\nimport { z } from 'zod'\nimport type { SettingConfig, SettingsDefinition } from './types.js'\n\n/**\n * Convert a single setting config to a Zod schema.\n */\nexport function settingToZod(config: SettingConfig): z.ZodType {\n switch (config.type) {\n case 'text': {\n let schema: z.ZodString = z.string()\n if (config.maxLength) schema = schema.max(config.maxLength)\n if (config.minLength) schema = schema.min(config.minLength)\n if (config.pattern) schema = schema.regex(config.pattern)\n return schema\n }\n\n case 'number': {\n let schema: z.ZodNumber = z.number()\n if (config.integer) schema = schema.int()\n if (config.min !== undefined) schema = schema.min(config.min)\n if (config.max !== undefined) schema = schema.max(config.max)\n return schema\n }\n\n case 'boolean':\n return z.boolean()\n\n case 'select':\n return z.enum(config.options as [string, ...string[]])\n\n case 'json':\n return config.schema ?? z.unknown()\n\n case 'media':\n return z.string().uuid()\n\n default:\n return z.unknown()\n }\n}\n\n/**\n * Generate a validation map for all settings in a definition.\n * Returns a Record<key, ZodType> for validating individual set() calls.\n *\n * Settings without a default are nullable (matching InferSettingValue),\n * so their validators accept null.\n */\nexport function generateSettingValidators(\n definition: SettingsDefinition,\n): Record<string, z.ZodType> {\n const validators: Record<string, z.ZodType> = {}\n for (const [key, config] of Object.entries(definition.schema)) {\n let schema = settingToZod(config)\n const hasDefault = 'default' in config && config.default !== undefined\n if (!hasDefault) {\n schema = schema.nullable()\n }\n validators[key] = schema\n }\n return validators\n}\n","/**\n * SettingsClient — typed CRUD for key-value settings.\n *\n * Server-only. Uses read-write DB connection.\n * Validates values against the setting schema on set().\n *\n * Supports per-locale values for settings marked `translatable: true`.\n * Non-translatable settings always use the `__default__` locale.\n *\n * All persistence goes through the `toolkit_settings` TableClient\n * (`defineTable`) — no direct Drizzle usage from this module.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n *\n * // Default locale\n * const name = await settings.get('siteName') // string (has default)\n *\n * // Locale-specific (only for translatable settings)\n * const nameEt = await settings.get('siteName', { locale: 'et' })\n *\n * await settings.set('siteName', 'Mänguväljak', { locale: 'et' })\n * ```\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { ZodType } from 'zod'\nimport { toolkitSettingsTable } from './schema.js'\nimport type {\n InferSettingsMap,\n InferSettingValue,\n SettingConfig,\n SettingScope,\n SettingsDefinition,\n} from './types.js'\nimport { DEFAULT_LOCALE, GLOBAL_SCOPE_ID } from './types.js'\nimport { generateSettingValidators } from './validation.js'\n\nexport interface SettingsClientConfig {\n /** Database client (read-write) */\n db: PostgresJsDatabase\n /** Logger instance */\n logger?: Logger\n /** Override scope (defaults to definition's scope) */\n scope?: SettingScope\n /** Scope ID (required for team/user scope, defaults to '__global__' for global) */\n scopeId?: string\n}\n\nexport interface LocaleOption {\n /** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */\n locale?: string\n}\n\n/** Row shape returned by the settings table (for narrow typing of fetched rows). */\ninterface SettingsRow {\n key: string\n locale: string\n value: unknown\n}\n\nexport class SettingsClient<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n private definition: SettingsDefinition<S>\n private table: ReturnType<typeof toolkitSettingsTable.makeClient>\n private logger?: Logger\n private scope: SettingScope\n private scopeId: string\n private validators: Record<string, ZodType>\n\n constructor(definition: SettingsDefinition<S>, config: SettingsClientConfig) {\n if (typeof window !== 'undefined') {\n throw new Error('SettingsClient cannot be used in browser code.')\n }\n\n this.definition = definition\n this.table = toolkitSettingsTable.makeClient(config.db)\n this.logger = config.logger\n this.scope = config.scope ?? definition.scope\n this.scopeId = config.scopeId ?? GLOBAL_SCOPE_ID\n\n if ((this.scope === 'team' || this.scope === 'user') && !config.scopeId) {\n throw new Error(\n `scopeId is required for ${this.scope}-scoped settings (namespace: ${definition.namespace})`,\n )\n }\n\n this.validators = generateSettingValidators(definition)\n }\n\n /**\n * Get a single setting value.\n *\n * For translatable settings with a locale, tries locale-specific value first,\n * then falls back to the default value, then the schema default, then null.\n */\n async get<K extends string & keyof S>(\n key: K,\n options?: LocaleOption,\n ): Promise<InferSettingValue<S[K]>> {\n const config = this.definition.schema[key]\n const locale = config?.translatable && options?.locale ? options.locale : null\n\n this.logger?.debug({ namespace: this.definition.namespace, key, locale }, 'Getting setting')\n\n if (locale) {\n // Fetch both locale-specific and default in one query\n const rows = (await this.table.findMany({\n where: {\n ...this.baseWhere(),\n key,\n $or: [{ locale }, { locale: DEFAULT_LOCALE }],\n },\n limit: 2,\n })) as unknown as SettingsRow[]\n\n // Prefer locale-specific, fall back to default\n const localeRow = rows.find((r) => r.locale === locale)\n const defaultRow = rows.find((r) => r.locale === DEFAULT_LOCALE)\n const row = localeRow ?? defaultRow\n\n if (row && row.value !== undefined && row.value !== null) {\n return row.value as InferSettingValue<S[K]>\n }\n } else {\n const rows = (await this.table.findMany({\n where: { ...this.baseWhere(), key, locale: DEFAULT_LOCALE },\n limit: 1,\n })) as unknown as SettingsRow[]\n\n if (rows.length > 0 && rows[0].value !== undefined && rows[0].value !== null) {\n return rows[0].value as InferSettingValue<S[K]>\n }\n }\n\n if (config && 'default' in config && config.default !== undefined) {\n return config.default as InferSettingValue<S[K]>\n }\n\n return null as InferSettingValue<S[K]>\n }\n\n /**\n * Get all settings for this namespace/scope as a typed object.\n * Missing values are filled from schema defaults.\n *\n * When locale is specified, translatable settings prefer the locale-specific\n * value over the default value.\n */\n async getAll(options?: LocaleOption): Promise<InferSettingsMap<S>> {\n const locale = options?.locale ?? null\n\n this.logger?.debug({ namespace: this.definition.namespace, locale }, 'Getting all settings')\n\n let rows: SettingsRow[]\n\n if (locale) {\n // Fetch both default and locale-specific rows\n rows = (await this.table.findMany({\n where: {\n ...this.baseWhere(),\n $or: [{ locale: DEFAULT_LOCALE }, { locale }],\n },\n limit: 1000,\n })) as unknown as SettingsRow[]\n } else {\n rows = (await this.table.findMany({\n where: { ...this.baseWhere(), locale: DEFAULT_LOCALE },\n limit: 1000,\n })) as unknown as SettingsRow[]\n }\n\n // Build lookup: key → { default: value, locale: value }\n const stored = new Map<string, { default?: unknown; locale?: unknown }>()\n for (const row of rows) {\n const entry = stored.get(row.key) ?? {}\n if (row.locale === DEFAULT_LOCALE) {\n entry.default = row.value\n } else {\n entry.locale = row.value\n }\n stored.set(row.key, entry)\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, config] of Object.entries(this.definition.schema)) {\n const entry = stored.get(key)\n\n // For translatable settings with locale, prefer locale-specific value\n let value: unknown\n if (config.translatable && locale && entry?.locale !== undefined && entry?.locale !== null) {\n value = entry.locale\n } else if (entry?.default !== undefined && entry?.default !== null) {\n value = entry.default\n }\n\n if (value !== undefined) {\n result[key] = value\n } else if ('default' in config && config.default !== undefined) {\n result[key] = config.default\n } else {\n result[key] = null\n }\n }\n\n return result as InferSettingsMap<S>\n }\n\n /**\n * Set a single setting value.\n * Validates against the schema before writing.\n *\n * Pass `{ locale }` to write a locale-specific value (only for translatable settings).\n */\n async set<K extends string & keyof S>(\n key: K,\n value: InferSettingValue<S[K]>,\n options?: LocaleOption,\n ): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Setting value')\n\n if (!(key in this.definition.schema)) {\n throw new Error(`Unknown setting key '${key}' in namespace '${this.definition.namespace}'`)\n }\n\n const validator = this.validators[key]\n if (validator) {\n validator.parse(value)\n }\n\n await this.upsertRow(key, value as unknown, locale, this.table)\n }\n\n /**\n * Set multiple settings at once (validated individually).\n * Writes in a transaction — all or nothing.\n *\n * Pass `{ locale }` to write locale-specific values for translatable settings.\n * Non-translatable settings in the values will always write to the default locale.\n */\n async setMany(values: Partial<InferSettingsMap<S>>, options?: LocaleOption): Promise<void> {\n this.logger?.info(\n { namespace: this.definition.namespace, keys: Object.keys(values), locale: options?.locale },\n 'Setting multiple values',\n )\n\n // Validate all first (fail fast)\n for (const [key, value] of Object.entries(values)) {\n if (!(key in this.definition.schema)) {\n throw new Error(`Unknown setting key '${key}' in namespace '${this.definition.namespace}'`)\n }\n const validator = this.validators[key]\n if (validator && value !== undefined) {\n validator.parse(value)\n }\n }\n\n await this.table.transaction(async (tx) => {\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n const locale = this.resolveLocale(key, options)\n await this.upsertRow(key, value as unknown, locale, tx)\n }\n })\n }\n\n /**\n * Delete a setting (resets to default on next get).\n * Pass `{ locale }` to delete only the locale-specific value.\n */\n async delete<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Deleting setting')\n\n await this.table.deleteMany({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n /**\n * Check if a setting has a stored value (not relying on default).\n */\n async has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean> {\n const locale = this.resolveLocale(key, options)\n\n return await this.table.exists({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n // ---------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------\n\n /**\n * Resolve which locale to use for a given key.\n * Non-translatable settings always use DEFAULT_LOCALE.\n * Translatable settings use the requested locale or DEFAULT_LOCALE.\n */\n private resolveLocale(key: string, options?: LocaleOption): string {\n const config = this.definition.schema[key as keyof S] as SettingConfig | undefined\n if (config?.translatable && options?.locale) {\n return options.locale\n }\n return DEFAULT_LOCALE\n }\n\n /** Base where conditions: namespace + scope + scopeId. */\n private baseWhere() {\n return {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n }\n }\n\n /**\n * Upsert a single setting row via the TableClient.\n * Accepts either the top-level table client or a transactional one —\n * same shape, used inside `setMany`'s transaction.\n */\n private async upsertRow(\n key: string,\n value: unknown,\n locale: string,\n tableClient: ReturnType<typeof toolkitSettingsTable.makeClient>,\n ) {\n await tableClient.upsert(\n {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n key,\n locale,\n value,\n updatedAt: new Date(),\n },\n {\n target: ['namespace', 'scope', 'scopeId', 'key', 'locale'],\n set: {\n value,\n updatedAt: new Date(),\n },\n },\n )\n }\n}\n","/**\n * Factory function for creating typed SettingsClient instances.\n *\n * Follows the createAdminClient(entity, app?) pattern from @murumets-ee/core.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { getApp } from '@murumets-ee/core'\nimport { SettingsClient } from './client.js'\nimport type { SettingConfig, SettingScope, SettingsDefinition } from './types.js'\n\nexport interface CreateSettingsClientOptions {\n /** Override the toolkit app (defaults to getApp()) */\n app?: ToolkitApp\n /** Override scope from definition */\n scope?: SettingScope\n /** Scope ID for team/user scoped settings */\n scopeId?: string\n}\n\n/**\n * Create a typed SettingsClient.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n * const name = await settings.get('siteName') // string\n * ```\n */\nexport function createSettingsClient<S extends Record<string, SettingConfig>>(\n definition: SettingsDefinition<S>,\n options?: CreateSettingsClientOptions,\n): SettingsClient<S> {\n const app = options?.app ?? getApp()\n\n return new SettingsClient(definition, {\n db: app.db.readWrite,\n logger: app.logger.child({ settings: definition.namespace }),\n scope: options?.scope,\n scopeId: options?.scopeId,\n })\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-factory-BwJW9_zW.d.mts","names":[],"sources":["../src/client.ts","../src/client-factory.ts"],"mappings":";;;;;UA2CiB,oBAAA;EAwB0C;EAtBzD,EAAA,EAAI,kBAAA;EA+BuC;EA7B3C,MAAA,GAAS,MAAA;EA6B8C;EA3BvD,KAAA,GAAQ,YAAA;EAsDD;EApDP,OAAA;AAAA;AAAA,UAGe,YAAA;EAmDJ;EAjDX,MAAA;AAAA;AAAA,cAUW,cAAA,WACD,MAAA,SAAe,aAAA,IAAiB,MAAA,SAAe,aAAA;EAAA,QAEjD,UAAA;EAAA,QACA,KAAA;EAAA,QACA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,OAAA;EAAA,QACA,UAAA;cAEI,UAAA,EAAY,kBAAA,CAAmB,CAAA,GAAI,MAAA,EAAQ,oBAAA;EAmJ3C;;;;;;EAzHN,GAAA,0BAA6B,CAAA,CAAA,CACjC,GAAA,EAAK,CAAA,EACL,OAAA,GAAU,YAAA,GACT,OAAA,CAAQ,iBAAA,CAAkB,CAAA,CAAE,CAAA;EA6KO;;;;;;;EA3HhC,MAAA,CAAO,OAAA,GAAU,YAAA,GAAe,OAAA,CAAQ,gBAAA,CAAiB,CAAA;EA0Ie;;;;;;EAzExE,GAAA,0BAA6B,CAAA,CAAA,CACjC,GAAA,EAAK,CAAA,EACL,KAAA,EAAO,iBAAA,CAAkB,CAAA,CAAE,CAAA,IAC3B,OAAA,GAAU,YAAA,GACT,OAAA;EA3JK;;;;;;;EAmLF,OAAA,CAAQ,MAAA,EAAQ,OAAA,CAAQ,gBAAA,CAAiB,CAAA,IAAK,OAAA,GAAU,YAAA,GAAe,OAAA;EA5KlC;;;;EA0MrC,MAAA,0BAAgC,CAAA,CAAA,CAAG,GAAA,EAAK,CAAA,EAAG,OAAA,GAAU,YAAA,GAAe,OAAA;EAhLhE;;;EA+LJ,GAAA,0BAA6B,CAAA,CAAA,CAAG,GAAA,EAAK,CAAA,EAAG,OAAA,GAAU,YAAA,GAAe,OAAA;EA7L3D;;;;;EAAA,QAgNJ,aAAA;EA7JF;EAAA,QAsKE,SAAA;EAtKK;;;;;EAAA,QAmLC,SAAA;AAAA;;;UCnUC,2BAAA;EDoCN;EClCT,GAAA,GAAM,UAAA;EDoCc;EClCpB,KAAA,GAAQ,YAAA;ED8BR;EC5BA,OAAA;AAAA;;;;;;;ADqCF;;;;;AAYA;iBClCgB,oBAAA,WAA+B,MAAA,SAAe,aAAA,EAAA,CAC5D,UAAA,EAAY,kBAAA,CAAmB,CAAA,GAC/B,OAAA,GAAU,2BAAA,GACT,cAAA,CAAe,CAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{getApp as e}from"@murumets-ee/core";import{column as t,defineTable as n}from"@murumets-ee/db";import{z as r}from"zod";const i=n({name:`toolkit_settings`,columns:{id:t.uuid({primaryKey:!0,defaultRandom:!0}),namespace:t.varchar({length:100,notNull:!0}),scope:t.varchar({length:20,notNull:!0,default:`global`}),scopeId:t.varchar({length:100,notNull:!0,default:`__global__`,pgName:`scope_id`}),key:t.varchar({length:255,notNull:!0}),locale:t.varchar({length:10,notNull:!0,default:`_default`}),value:t.jsonb(),updatedAt:t.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`}),updatedBy:t.uuid({pgName:`updated_by`})},unique:[{on:[`namespace`,`scope`,`scopeId`,`key`,`locale`]}]});i.table,n({name:`toolkit_view_state`,columns:{id:t.uuid({primaryKey:!0,defaultRandom:!0}),userId:t.uuid({notNull:!0,pgName:`user_id`}),viewKey:t.varchar({length:255,notNull:!0,pgName:`view_key`}),state:t.jsonb({notNull:!0}),expiresAt:t.timestamp({withTimezone:!0,pgName:`expires_at`}),updatedAt:t.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`userId`,`viewKey`]}]}).table;const a=`_default`;function o(e){switch(e.type){case`text`:{let t=r.string();return e.maxLength&&(t=t.max(e.maxLength)),e.minLength&&(t=t.min(e.minLength)),e.pattern&&(t=t.regex(e.pattern)),t}case`number`:{let t=r.number();return e.integer&&(t=t.int()),e.min!==void 0&&(t=t.min(e.min)),e.max!==void 0&&(t=t.max(e.max)),t}case`boolean`:return r.boolean();case`select`:return r.enum(e.options);case`json`:return e.schema??r.unknown();case`media`:return r.string().uuid();default:return r.unknown()}}function s(e){let t={};for(let[n,r]of Object.entries(e.schema)){let e=o(r);`default`in r&&r.default!==void 0||(e=e.nullable()),t[n]=e}return t}var c=class{definition;table;logger;scope;scopeId;validators;constructor(e,t){if(typeof window<`u`)throw Error(`SettingsClient cannot be used in browser code.`);if(this.definition=e,this.table=i.makeClient(t.db),this.logger=t.logger,this.scope=t.scope??e.scope,this.scopeId=t.scopeId??`__global__`,(this.scope===`team`||this.scope===`user`)&&!t.scopeId)throw Error(`scopeId is required for ${this.scope}-scoped settings (namespace: ${e.namespace})`);this.validators=s(e)}async get(e,t){let n=this.definition.schema[e],r=n?.translatable&&t?.locale?t.locale:null;if(this.logger?.debug({namespace:this.definition.namespace,key:e,locale:r},`Getting setting`),r){let t=await this.table.findMany({where:{...this.baseWhere(),key:e,$or:[{locale:r},{locale:a}]},limit:2}),n=t.find(e=>e.locale===r),i=t.find(e=>e.locale===a),o=n??i;if(o&&o.value!==void 0&&o.value!==null)return o.value}else{let t=await this.table.findMany({where:{...this.baseWhere(),key:e,locale:a},limit:1});if(t.length>0&&t[0].value!==void 0&&t[0].value!==null)return t[0].value}return n&&`default`in n&&n.default!==void 0?n.default:null}async getAll(e){let t=e?.locale??null;this.logger?.debug({namespace:this.definition.namespace,locale:t},`Getting all settings`);let n;n=t?await this.table.findMany({where:{...this.baseWhere(),$or:[{locale:a},{locale:t}]},limit:1e3}):await this.table.findMany({where:{...this.baseWhere(),locale:a},limit:1e3});let r=new Map;for(let e of n){let t=r.get(e.key)??{};e.locale===`_default`?t.default=e.value:t.locale=e.value,r.set(e.key,t)}let i={};for(let[e,n]of Object.entries(this.definition.schema)){let a=r.get(e),o;n.translatable&&t&&a?.locale!==void 0&&a?.locale!==null?o=a.locale:a?.default!==void 0&&a?.default!==null&&(o=a.default),o===void 0?`default`in n&&n.default!==void 0?i[e]=n.default:i[e]=null:i[e]=o}return i}async set(e,t,n){let r=this.resolveLocale(e,n);if(this.logger?.info({namespace:this.definition.namespace,key:e,locale:r},`Setting value`),!(e in this.definition.schema))throw Error(`Unknown setting key '${e}' in namespace '${this.definition.namespace}'`);let i=this.validators[e];i&&i.parse(t),await this.upsertRow(e,t,r,this.table)}async setMany(e,t){this.logger?.info({namespace:this.definition.namespace,keys:Object.keys(e),locale:t?.locale},`Setting multiple values`);for(let[t,n]of Object.entries(e)){if(!(t in this.definition.schema))throw Error(`Unknown setting key '${t}' in namespace '${this.definition.namespace}'`);let e=this.validators[t];e&&n!==void 0&&e.parse(n)}await this.table.transaction(async n=>{for(let[r,i]of Object.entries(e)){if(i===void 0)continue;let e=this.resolveLocale(r,t);await this.upsertRow(r,i,e,n)}})}async delete(e,t){let n=this.resolveLocale(e,t);this.logger?.info({namespace:this.definition.namespace,key:e,locale:n},`Deleting setting`),await this.table.deleteMany({...this.baseWhere(),key:e,locale:n})}async has(e,t){let n=this.resolveLocale(e,t);return await this.table.exists({...this.baseWhere(),key:e,locale:n})}resolveLocale(e,t){return this.definition.schema[e]?.translatable&&t?.locale?t.locale:a}baseWhere(){return{namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId}}async upsertRow(e,t,n,r){await r.upsert({namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId,key:e,locale:n,value:t,updatedAt:new Date},{target:[`namespace`,`scope`,`scopeId`,`key`,`locale`],set:{value:t,updatedAt:new Date}})}};function l(t,n){let r=n?.app??e();return new c(t,{db:r.db.readWrite,logger:r.logger.child({settings:t.namespace}),scope:n?.scope,scopeId:n?.scopeId})}export{l as createSettingsClient};
|
|
2
|
-
//# sourceMappingURL=client-factory-DBlcuyG0.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-factory-DBlcuyG0.mjs","names":[],"sources":["../src/schema.ts","../src/types.ts","../src/validation.ts","../src/client.ts","../src/client-factory.ts"],"sourcesContent":["/**\n * Drizzle schema for settings tables.\n *\n * Two tables:\n * - toolkit_settings: typed key-value settings grouped by namespace and scope\n * - toolkit_view_state: schemaless user-scoped JSON blobs with optional TTL\n *\n * These tables are registered for migration discovery via the `settings()`\n * plugin's `tables` field. `lumi migrate` picks them up automatically.\n *\n * Built on `@murumets-ee/db`'s `defineTable` — the raw `pgTable` is still\n * re-exported (via `.table`) for backwards compatibility with existing\n * imports and tests, but all query construction in SettingsClient /\n * ViewStateClient goes through the typed TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\n/**\n * Typed settings table.\n *\n * Stores key-value pairs grouped by namespace and scoped\n * to global, team, or user contexts.\n *\n * scopeId uses '__global__' sentinel for global scope to avoid\n * PostgreSQL's NULL != NULL behavior in unique constraints.\n */\nexport const toolkitSettingsTable = defineTable({\n name: 'toolkit_settings',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n namespace: column.varchar({ length: 100, notNull: true }),\n scope: column.varchar({ length: 20, notNull: true, default: 'global' }),\n scopeId: column.varchar({\n length: 100,\n notNull: true,\n default: '__global__',\n pgName: 'scope_id',\n }),\n key: column.varchar({ length: 255, notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_default' }),\n value: column.jsonb(),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n updatedBy: column.uuid({ pgName: 'updated_by' }),\n },\n unique: [\n { on: ['namespace', 'scope', 'scopeId', 'key', 'locale'] },\n ],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitSettings = toolkitSettingsTable.table\n\n/**\n * View state table.\n *\n * Stores schemaless user-scoped JSON blobs for persisting\n * UI state (table filters, column order, etc.) with optional TTL.\n */\nexport const toolkitViewStateTable = defineTable({\n name: 'toolkit_view_state',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n userId: column.uuid({ notNull: true, pgName: 'user_id' }),\n viewKey: column.varchar({ length: 255, notNull: true, pgName: 'view_key' }),\n state: column.jsonb({ notNull: true }),\n expiresAt: column.timestamp({ withTimezone: true, pgName: 'expires_at' }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [\n { on: ['userId', 'viewKey'] },\n ],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitViewState = toolkitViewStateTable.table\n","/**\n * Setting configuration types and compile-time type inference.\n *\n * Design mirrors the entity field system:\n * - Config interfaces define what each setting type accepts\n * - SettingToTS maps a single config to its TypeScript type\n * - InferSettingValue adds null awareness based on `default` presence\n * - InferSettingsMap maps an entire schema to a typed record\n */\n\nimport type { ZodType } from 'zod'\n\n// ---------------------------------------------------------------\n// 1. Setting config interfaces\n// ---------------------------------------------------------------\n\nexport interface BaseSettingConfig {\n /** Human-readable label for admin UI */\n label?: string\n /** Description / help text */\n description?: string\n /** If true, this setting can have per-locale values (mirrors entity translatable pattern) */\n translatable?: boolean\n}\n\nexport interface TextSettingConfig extends BaseSettingConfig {\n type: 'text'\n default?: string\n maxLength?: number\n minLength?: number\n pattern?: RegExp\n /** Render as textarea instead of single-line input. */\n multiline?: boolean\n}\n\nexport interface NumberSettingConfig extends BaseSettingConfig {\n type: 'number'\n default?: number\n min?: number\n max?: number\n integer?: boolean\n}\n\nexport interface BooleanSettingConfig extends BaseSettingConfig {\n type: 'boolean'\n default?: boolean\n}\n\nexport interface SelectSettingConfig<O extends readonly string[] = readonly string[]>\n extends BaseSettingConfig {\n type: 'select'\n options: O\n default?: O[number]\n}\n\nexport interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {\n type: 'json'\n default?: T\n /** Optional Zod schema for validation. If provided, values are validated on set. */\n schema?: ZodType<T>\n}\n\nexport interface MediaSettingConfig extends BaseSettingConfig {\n type: 'media'\n default?: string\n accept?: string[]\n}\n\nexport type SettingConfig =\n | TextSettingConfig\n | NumberSettingConfig\n | BooleanSettingConfig\n | SelectSettingConfig\n | JsonSettingConfig\n | MediaSettingConfig\n\n// ---------------------------------------------------------------\n// 2. Setting-to-TypeScript mapping (single setting)\n// ---------------------------------------------------------------\n\n/**\n * Maps a single SettingConfig to its TypeScript output type.\n * Each branch is a shallow comparison — no recursion.\n */\nexport type SettingToTS<S extends SettingConfig> = S extends TextSettingConfig\n ? string\n : S extends NumberSettingConfig\n ? number\n : S extends BooleanSettingConfig\n ? boolean\n : S extends SelectSettingConfig<infer O>\n ? O[number]\n : S extends JsonSettingConfig<infer T>\n ? T\n : S extends MediaSettingConfig\n ? string\n : never\n\n// ---------------------------------------------------------------\n// 3. Null awareness: settings with defaults always return value\n// ---------------------------------------------------------------\n\n/**\n * If a setting has a `default`, get() never returns null.\n * Without a default, it returns T | null.\n */\nexport type InferSettingValue<S extends SettingConfig> = S extends { default: unknown }\n ? SettingToTS<S>\n : SettingToTS<S> | null\n\n// ---------------------------------------------------------------\n// 4. Full settings map type (what getAll() returns)\n// ---------------------------------------------------------------\n\nexport type InferSettingsMap<Schema extends Record<string, SettingConfig>> = {\n [K in keyof Schema]: InferSettingValue<Schema[K]>\n}\n\n// ---------------------------------------------------------------\n// 5. Scope types\n// ---------------------------------------------------------------\n\nexport type SettingScope = 'global' | 'team' | 'user'\n\n/** Sentinel value for global scope_id (avoids NULL uniqueness issues) */\nexport const GLOBAL_SCOPE_ID = '__global__'\n\n/** Sentinel value for locale column when no locale is specified (base/default value) */\nexport const DEFAULT_LOCALE = '_default'\n\n// ---------------------------------------------------------------\n// 6. Settings definition (returned by defineSettings)\n// ---------------------------------------------------------------\n\nexport interface SettingsDefinition<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n /** Unique namespace for this settings group */\n namespace: string\n /** Default scope for these settings */\n scope: SettingScope\n /** Setting schema (the shape) */\n schema: S\n /** Human-readable label for admin UI */\n label?: string\n /**\n * Lucide icon name (e.g. `'globe'`, `'ticket'`) for the sidebar nav\n * entry this definition produces. Falls back to `'settings'` when\n * unset.\n */\n iconName?: string\n /**\n * Hide this namespace from the auto-generated sidebar nav. Still\n * renders in the settings API. Useful for internal-only namespaces\n * like the permissions store.\n */\n hideFromMenu?: boolean\n}\n","/**\n * Generate Zod validation schemas from setting config definitions.\n * Validates values before writing to the database.\n */\n\nimport { z } from 'zod'\nimport type { SettingConfig, SettingsDefinition } from './types.js'\n\n/**\n * Convert a single setting config to a Zod schema.\n */\nexport function settingToZod(config: SettingConfig): z.ZodType {\n switch (config.type) {\n case 'text': {\n let schema: z.ZodString = z.string()\n if (config.maxLength) schema = schema.max(config.maxLength)\n if (config.minLength) schema = schema.min(config.minLength)\n if (config.pattern) schema = schema.regex(config.pattern)\n return schema\n }\n\n case 'number': {\n let schema: z.ZodNumber = z.number()\n if (config.integer) schema = schema.int()\n if (config.min !== undefined) schema = schema.min(config.min)\n if (config.max !== undefined) schema = schema.max(config.max)\n return schema\n }\n\n case 'boolean':\n return z.boolean()\n\n case 'select':\n return z.enum(config.options as [string, ...string[]])\n\n case 'json':\n return config.schema ?? z.unknown()\n\n case 'media':\n return z.string().uuid()\n\n default:\n return z.unknown()\n }\n}\n\n/**\n * Generate a validation map for all settings in a definition.\n * Returns a Record<key, ZodType> for validating individual set() calls.\n *\n * Settings without a default are nullable (matching InferSettingValue),\n * so their validators accept null.\n */\nexport function generateSettingValidators(\n definition: SettingsDefinition,\n): Record<string, z.ZodType> {\n const validators: Record<string, z.ZodType> = {}\n for (const [key, config] of Object.entries(definition.schema)) {\n let schema = settingToZod(config)\n const hasDefault = 'default' in config && config.default !== undefined\n if (!hasDefault) {\n schema = schema.nullable()\n }\n validators[key] = schema\n }\n return validators\n}\n","/**\n * SettingsClient — typed CRUD for key-value settings.\n *\n * Server-only. Uses read-write DB connection.\n * Validates values against the setting schema on set().\n *\n * Supports per-locale values for settings marked `translatable: true`.\n * Non-translatable settings always use the `__default__` locale.\n *\n * All persistence goes through the `toolkit_settings` TableClient\n * (`defineTable`) — no direct Drizzle usage from this module.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n *\n * // Default locale\n * const name = await settings.get('siteName') // string (has default)\n *\n * // Locale-specific (only for translatable settings)\n * const nameEt = await settings.get('siteName', { locale: 'et' })\n *\n * await settings.set('siteName', 'Mänguväljak', { locale: 'et' })\n * ```\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { ZodType } from 'zod'\nimport { toolkitSettingsTable } from './schema.js'\nimport type {\n InferSettingsMap,\n InferSettingValue,\n SettingConfig,\n SettingScope,\n SettingsDefinition,\n} from './types.js'\nimport { DEFAULT_LOCALE, GLOBAL_SCOPE_ID } from './types.js'\nimport { generateSettingValidators } from './validation.js'\n\nexport interface SettingsClientConfig {\n /** Database client (read-write) */\n db: PostgresJsDatabase\n /** Logger instance */\n logger?: Logger\n /** Override scope (defaults to definition's scope) */\n scope?: SettingScope\n /** Scope ID (required for team/user scope, defaults to '__global__' for global) */\n scopeId?: string\n}\n\nexport interface LocaleOption {\n /** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */\n locale?: string\n}\n\n/** Row shape returned by the settings table (for narrow typing of fetched rows). */\ninterface SettingsRow {\n key: string\n locale: string\n value: unknown\n}\n\nexport class SettingsClient<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n private definition: SettingsDefinition<S>\n private table: ReturnType<typeof toolkitSettingsTable.makeClient>\n private logger?: Logger\n private scope: SettingScope\n private scopeId: string\n private validators: Record<string, ZodType>\n\n constructor(definition: SettingsDefinition<S>, config: SettingsClientConfig) {\n if (typeof window !== 'undefined') {\n throw new Error('SettingsClient cannot be used in browser code.')\n }\n\n this.definition = definition\n this.table = toolkitSettingsTable.makeClient(config.db)\n this.logger = config.logger\n this.scope = config.scope ?? definition.scope\n this.scopeId = config.scopeId ?? GLOBAL_SCOPE_ID\n\n if ((this.scope === 'team' || this.scope === 'user') && !config.scopeId) {\n throw new Error(\n `scopeId is required for ${this.scope}-scoped settings (namespace: ${definition.namespace})`,\n )\n }\n\n this.validators = generateSettingValidators(definition)\n }\n\n /**\n * Get a single setting value.\n *\n * For translatable settings with a locale, tries locale-specific value first,\n * then falls back to the default value, then the schema default, then null.\n */\n async get<K extends string & keyof S>(\n key: K,\n options?: LocaleOption,\n ): Promise<InferSettingValue<S[K]>> {\n const config = this.definition.schema[key]\n const locale = config?.translatable && options?.locale ? options.locale : null\n\n this.logger?.debug({ namespace: this.definition.namespace, key, locale }, 'Getting setting')\n\n if (locale) {\n // Fetch both locale-specific and default in one query\n const rows = (await this.table.findMany({\n where: {\n ...this.baseWhere(),\n key,\n $or: [{ locale }, { locale: DEFAULT_LOCALE }],\n },\n limit: 2,\n })) as unknown as SettingsRow[]\n\n // Prefer locale-specific, fall back to default\n const localeRow = rows.find((r) => r.locale === locale)\n const defaultRow = rows.find((r) => r.locale === DEFAULT_LOCALE)\n const row = localeRow ?? defaultRow\n\n if (row && row.value !== undefined && row.value !== null) {\n return row.value as InferSettingValue<S[K]>\n }\n } else {\n const rows = (await this.table.findMany({\n where: { ...this.baseWhere(), key, locale: DEFAULT_LOCALE },\n limit: 1,\n })) as unknown as SettingsRow[]\n\n if (rows.length > 0 && rows[0].value !== undefined && rows[0].value !== null) {\n return rows[0].value as InferSettingValue<S[K]>\n }\n }\n\n if (config && 'default' in config && config.default !== undefined) {\n return config.default as InferSettingValue<S[K]>\n }\n\n return null as InferSettingValue<S[K]>\n }\n\n /**\n * Get all settings for this namespace/scope as a typed object.\n * Missing values are filled from schema defaults.\n *\n * When locale is specified, translatable settings prefer the locale-specific\n * value over the default value.\n */\n async getAll(options?: LocaleOption): Promise<InferSettingsMap<S>> {\n const locale = options?.locale ?? null\n\n this.logger?.debug({ namespace: this.definition.namespace, locale }, 'Getting all settings')\n\n let rows: SettingsRow[]\n\n if (locale) {\n // Fetch both default and locale-specific rows\n rows = (await this.table.findMany({\n where: {\n ...this.baseWhere(),\n $or: [{ locale: DEFAULT_LOCALE }, { locale }],\n },\n limit: 1000,\n })) as unknown as SettingsRow[]\n } else {\n rows = (await this.table.findMany({\n where: { ...this.baseWhere(), locale: DEFAULT_LOCALE },\n limit: 1000,\n })) as unknown as SettingsRow[]\n }\n\n // Build lookup: key → { default: value, locale: value }\n const stored = new Map<string, { default?: unknown; locale?: unknown }>()\n for (const row of rows) {\n const entry = stored.get(row.key) ?? {}\n if (row.locale === DEFAULT_LOCALE) {\n entry.default = row.value\n } else {\n entry.locale = row.value\n }\n stored.set(row.key, entry)\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, config] of Object.entries(this.definition.schema)) {\n const entry = stored.get(key)\n\n // For translatable settings with locale, prefer locale-specific value\n let value: unknown\n if (config.translatable && locale && entry?.locale !== undefined && entry?.locale !== null) {\n value = entry.locale\n } else if (entry?.default !== undefined && entry?.default !== null) {\n value = entry.default\n }\n\n if (value !== undefined) {\n result[key] = value\n } else if ('default' in config && config.default !== undefined) {\n result[key] = config.default\n } else {\n result[key] = null\n }\n }\n\n return result as InferSettingsMap<S>\n }\n\n /**\n * Set a single setting value.\n * Validates against the schema before writing.\n *\n * Pass `{ locale }` to write a locale-specific value (only for translatable settings).\n */\n async set<K extends string & keyof S>(\n key: K,\n value: InferSettingValue<S[K]>,\n options?: LocaleOption,\n ): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Setting value')\n\n if (!(key in this.definition.schema)) {\n throw new Error(`Unknown setting key '${key}' in namespace '${this.definition.namespace}'`)\n }\n\n const validator = this.validators[key]\n if (validator) {\n validator.parse(value)\n }\n\n await this.upsertRow(key, value as unknown, locale, this.table)\n }\n\n /**\n * Set multiple settings at once (validated individually).\n * Writes in a transaction — all or nothing.\n *\n * Pass `{ locale }` to write locale-specific values for translatable settings.\n * Non-translatable settings in the values will always write to the default locale.\n */\n async setMany(values: Partial<InferSettingsMap<S>>, options?: LocaleOption): Promise<void> {\n this.logger?.info(\n { namespace: this.definition.namespace, keys: Object.keys(values), locale: options?.locale },\n 'Setting multiple values',\n )\n\n // Validate all first (fail fast)\n for (const [key, value] of Object.entries(values)) {\n if (!(key in this.definition.schema)) {\n throw new Error(`Unknown setting key '${key}' in namespace '${this.definition.namespace}'`)\n }\n const validator = this.validators[key]\n if (validator && value !== undefined) {\n validator.parse(value)\n }\n }\n\n await this.table.transaction(async (tx) => {\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n const locale = this.resolveLocale(key, options)\n await this.upsertRow(key, value as unknown, locale, tx)\n }\n })\n }\n\n /**\n * Delete a setting (resets to default on next get).\n * Pass `{ locale }` to delete only the locale-specific value.\n */\n async delete<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<void> {\n const locale = this.resolveLocale(key, options)\n\n this.logger?.info({ namespace: this.definition.namespace, key, locale }, 'Deleting setting')\n\n await this.table.deleteMany({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n /**\n * Check if a setting has a stored value (not relying on default).\n */\n async has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean> {\n const locale = this.resolveLocale(key, options)\n\n return await this.table.exists({\n ...this.baseWhere(),\n key,\n locale,\n })\n }\n\n // ---------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------\n\n /**\n * Resolve which locale to use for a given key.\n * Non-translatable settings always use DEFAULT_LOCALE.\n * Translatable settings use the requested locale or DEFAULT_LOCALE.\n */\n private resolveLocale(key: string, options?: LocaleOption): string {\n const config = this.definition.schema[key as keyof S] as SettingConfig | undefined\n if (config?.translatable && options?.locale) {\n return options.locale\n }\n return DEFAULT_LOCALE\n }\n\n /** Base where conditions: namespace + scope + scopeId. */\n private baseWhere() {\n return {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n }\n }\n\n /**\n * Upsert a single setting row via the TableClient.\n * Accepts either the top-level table client or a transactional one —\n * same shape, used inside `setMany`'s transaction.\n */\n private async upsertRow(\n key: string,\n value: unknown,\n locale: string,\n tableClient: ReturnType<typeof toolkitSettingsTable.makeClient>,\n ) {\n await tableClient.upsert(\n {\n namespace: this.definition.namespace,\n scope: this.scope,\n scopeId: this.scopeId,\n key,\n locale,\n value,\n updatedAt: new Date(),\n },\n {\n target: ['namespace', 'scope', 'scopeId', 'key', 'locale'],\n set: {\n value,\n updatedAt: new Date(),\n },\n },\n )\n }\n}\n","/**\n * Factory function for creating typed SettingsClient instances.\n *\n * Follows the createAdminClient(entity, app?) pattern from @murumets-ee/core.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { getApp } from '@murumets-ee/core'\nimport { SettingsClient } from './client.js'\nimport type { SettingConfig, SettingScope, SettingsDefinition } from './types.js'\n\nexport interface CreateSettingsClientOptions {\n /** Override the toolkit app (defaults to getApp()) */\n app?: ToolkitApp\n /** Override scope from definition */\n scope?: SettingScope\n /** Scope ID for team/user scoped settings */\n scopeId?: string\n}\n\n/**\n * Create a typed SettingsClient.\n *\n * @example\n * ```typescript\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { siteSettings } from './settings/site'\n *\n * const settings = createSettingsClient(siteSettings)\n * const name = await settings.get('siteName') // string\n * ```\n */\nexport function createSettingsClient<S extends Record<string, SettingConfig>>(\n definition: SettingsDefinition<S>,\n options?: CreateSettingsClientOptions,\n): SettingsClient<S> {\n const app = options?.app ?? getApp()\n\n return new SettingsClient(definition, {\n db: app.db.readWrite,\n logger: app.logger.child({ settings: definition.namespace }),\n scope: options?.scope,\n scopeId: options?.scopeId,\n })\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e={text:e=>({type:`text`,...e}),number:e=>({type:`number`,...e}),boolean:e=>({type:`boolean`,...e}),select:e=>({type:`select`,...e}),json:e=>({type:`json`,...e}),media:e=>({type:`media`,...e})},t=`__global__`,n=`_default`;function r(e){if(!e.namespace)throw Error(`Settings namespace is required`);if(!e.schema||Object.keys(e.schema).length===0)throw Error(`Settings schema must have at least one setting`);return e}export{e as i,n,t as r,r as t};
|
|
2
|
-
//# sourceMappingURL=define-settings-Bi3STtAH.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-settings-Bi3STtAH.mjs","names":[],"sources":["../src/builders.ts","../src/types.ts","../src/define-settings.ts"],"sourcesContent":["/**\n * Fluent API for building setting definitions.\n *\n * Each builder uses a `const` generic parameter on the config to preserve\n * literal types (e.g., `default: 'My Site'` stays literal, not `string`).\n * This enables compile-time type inference in the settings system.\n *\n * Pattern matches packages/entity/src/fields/builders.ts exactly.\n */\n\nimport type {\n BooleanSettingConfig,\n JsonSettingConfig,\n MediaSettingConfig,\n NumberSettingConfig,\n SelectSettingConfig,\n TextSettingConfig,\n} from './types.js'\n\nexport const setting = {\n /**\n * Text setting (string value)\n */\n // biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\n text: <const C extends Partial<Omit<TextSettingConfig, 'type'>> = {}>(\n config?: C,\n ): TextSettingConfig & C => ({ type: 'text', ...config }) as TextSettingConfig & C,\n\n /**\n * Number setting\n */\n // biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\n number: <const C extends Partial<Omit<NumberSettingConfig, 'type'>> = {}>(\n config?: C,\n ): NumberSettingConfig & C => ({ type: 'number', ...config }) as NumberSettingConfig & C,\n\n /**\n * Boolean setting\n */\n // biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\n boolean: <const C extends Partial<Omit<BooleanSettingConfig, 'type'>> = {}>(\n config?: C,\n ): BooleanSettingConfig & C => ({ type: 'boolean', ...config }) as BooleanSettingConfig & C,\n\n /**\n * Select setting (enum from options tuple)\n * Preserves literal option types for type inference.\n *\n * @example\n * setting.select({ options: ['light', 'dark', 'system'] as const, default: 'system' })\n * // inferred type: 'light' | 'dark' | 'system'\n */\n select: <\n const O extends readonly string[],\n // biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\n const C extends Partial<Omit<SelectSettingConfig, 'type' | 'options'>> = {},\n >(\n config: { options: O } & C,\n ): SelectSettingConfig<O> & C => ({ type: 'select', ...config }) as SelectSettingConfig<O> & C,\n\n /**\n * JSON setting (arbitrary typed JSON)\n *\n * @example\n * setting.json<{ twitter?: string; github?: string }>()\n * setting.json<string[]>({ default: [] })\n */\n // biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\n json: <T = unknown, const C extends Partial<Omit<JsonSettingConfig<T>, 'type'>> = {}>(\n config?: C,\n ): JsonSettingConfig<T> & C => ({ type: 'json', ...config }) as JsonSettingConfig<T> & C,\n\n /**\n * Media setting (stores media ID as string UUID)\n */\n // biome-ignore lint/complexity/noBannedTypes: {} is correct as empty generic default\n media: <const C extends Partial<Omit<MediaSettingConfig, 'type'>> = {}>(\n config?: C,\n ): MediaSettingConfig & C => ({ type: 'media', ...config }) as MediaSettingConfig & C,\n}\n","/**\n * Setting configuration types and compile-time type inference.\n *\n * Design mirrors the entity field system:\n * - Config interfaces define what each setting type accepts\n * - SettingToTS maps a single config to its TypeScript type\n * - InferSettingValue adds null awareness based on `default` presence\n * - InferSettingsMap maps an entire schema to a typed record\n */\n\nimport type { ZodType } from 'zod'\n\n// ---------------------------------------------------------------\n// 1. Setting config interfaces\n// ---------------------------------------------------------------\n\nexport interface BaseSettingConfig {\n /** Human-readable label for admin UI */\n label?: string\n /** Description / help text */\n description?: string\n /** If true, this setting can have per-locale values (mirrors entity translatable pattern) */\n translatable?: boolean\n}\n\nexport interface TextSettingConfig extends BaseSettingConfig {\n type: 'text'\n default?: string\n maxLength?: number\n minLength?: number\n pattern?: RegExp\n /** Render as textarea instead of single-line input. */\n multiline?: boolean\n}\n\nexport interface NumberSettingConfig extends BaseSettingConfig {\n type: 'number'\n default?: number\n min?: number\n max?: number\n integer?: boolean\n}\n\nexport interface BooleanSettingConfig extends BaseSettingConfig {\n type: 'boolean'\n default?: boolean\n}\n\nexport interface SelectSettingConfig<O extends readonly string[] = readonly string[]>\n extends BaseSettingConfig {\n type: 'select'\n options: O\n default?: O[number]\n}\n\nexport interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {\n type: 'json'\n default?: T\n /** Optional Zod schema for validation. If provided, values are validated on set. */\n schema?: ZodType<T>\n}\n\nexport interface MediaSettingConfig extends BaseSettingConfig {\n type: 'media'\n default?: string\n accept?: string[]\n}\n\nexport type SettingConfig =\n | TextSettingConfig\n | NumberSettingConfig\n | BooleanSettingConfig\n | SelectSettingConfig\n | JsonSettingConfig\n | MediaSettingConfig\n\n// ---------------------------------------------------------------\n// 2. Setting-to-TypeScript mapping (single setting)\n// ---------------------------------------------------------------\n\n/**\n * Maps a single SettingConfig to its TypeScript output type.\n * Each branch is a shallow comparison — no recursion.\n */\nexport type SettingToTS<S extends SettingConfig> = S extends TextSettingConfig\n ? string\n : S extends NumberSettingConfig\n ? number\n : S extends BooleanSettingConfig\n ? boolean\n : S extends SelectSettingConfig<infer O>\n ? O[number]\n : S extends JsonSettingConfig<infer T>\n ? T\n : S extends MediaSettingConfig\n ? string\n : never\n\n// ---------------------------------------------------------------\n// 3. Null awareness: settings with defaults always return value\n// ---------------------------------------------------------------\n\n/**\n * If a setting has a `default`, get() never returns null.\n * Without a default, it returns T | null.\n */\nexport type InferSettingValue<S extends SettingConfig> = S extends { default: unknown }\n ? SettingToTS<S>\n : SettingToTS<S> | null\n\n// ---------------------------------------------------------------\n// 4. Full settings map type (what getAll() returns)\n// ---------------------------------------------------------------\n\nexport type InferSettingsMap<Schema extends Record<string, SettingConfig>> = {\n [K in keyof Schema]: InferSettingValue<Schema[K]>\n}\n\n// ---------------------------------------------------------------\n// 5. Scope types\n// ---------------------------------------------------------------\n\nexport type SettingScope = 'global' | 'team' | 'user'\n\n/** Sentinel value for global scope_id (avoids NULL uniqueness issues) */\nexport const GLOBAL_SCOPE_ID = '__global__'\n\n/** Sentinel value for locale column when no locale is specified (base/default value) */\nexport const DEFAULT_LOCALE = '_default'\n\n// ---------------------------------------------------------------\n// 6. Settings definition (returned by defineSettings)\n// ---------------------------------------------------------------\n\nexport interface SettingsDefinition<\n S extends Record<string, SettingConfig> = Record<string, SettingConfig>,\n> {\n /** Unique namespace for this settings group */\n namespace: string\n /** Default scope for these settings */\n scope: SettingScope\n /** Setting schema (the shape) */\n schema: S\n /** Human-readable label for admin UI */\n label?: string\n /**\n * Lucide icon name (e.g. `'globe'`, `'ticket'`) for the sidebar nav\n * entry this definition produces. Falls back to `'settings'` when\n * unset.\n */\n iconName?: string\n /**\n * Hide this namespace from the auto-generated sidebar nav. Still\n * renders in the settings API. Useful for internal-only namespaces\n * like the permissions store.\n */\n hideFromMenu?: boolean\n}\n","/**\n * Define a typed settings group.\n *\n * @example\n * ```typescript\n * import { defineSettings, setting } from '@murumets-ee/settings'\n *\n * export const siteSettings = defineSettings({\n * namespace: 'site',\n * scope: 'global',\n * schema: {\n * siteName: setting.text({ default: 'My Site' }),\n * logo: setting.media(),\n * maintenance: setting.boolean({ default: false }),\n * },\n * })\n * ```\n */\n\nimport type { SettingConfig, SettingScope, SettingsDefinition } from './types.js'\n\nexport function defineSettings<const S extends Record<string, SettingConfig>>(definition: {\n namespace: string\n scope: SettingScope\n schema: S\n label?: string\n}): SettingsDefinition<S> {\n if (!definition.namespace) {\n throw new Error('Settings namespace is required')\n }\n if (!definition.schema || Object.keys(definition.schema).length === 0) {\n throw new Error('Settings schema must have at least one setting')\n }\n return definition\n}\n"],"mappings":"AAmBA,MAAa,EAAU,CAKrB,KACE,IAC2B,CAAE,KAAM,OAAQ,GAAG,EAAQ,EAMxD,OACE,IAC6B,CAAE,KAAM,SAAU,GAAG,EAAQ,EAM5D,QACE,IAC8B,CAAE,KAAM,UAAW,GAAG,EAAQ,EAU9D,OAKE,IACgC,CAAE,KAAM,SAAU,GAAG,EAAQ,EAU/D,KACE,IAC8B,CAAE,KAAM,OAAQ,GAAG,EAAQ,EAM3D,MACE,IAC4B,CAAE,KAAM,QAAS,GAAG,EAAQ,EAC3D,CC8CY,EAAkB,aAGlB,EAAiB,WC3G9B,SAAgB,EAA8D,EAKpD,CACxB,GAAI,CAAC,EAAW,UACd,MAAU,MAAM,iCAAiC,CAEnD,GAAI,CAAC,EAAW,QAAU,OAAO,KAAK,EAAW,OAAO,CAAC,SAAW,EAClE,MAAU,MAAM,iDAAiD,CAEnE,OAAO"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-settings-YygzoniQ.d.mts","names":[],"sources":["../src/builders.ts","../src/define-settings.ts"],"mappings":";;;cAmBa,OAAA;EAOY;;;yBAFA,OAAA,CAAQ,IAAA,CAAK,iBAAA,iBAA2B,MAAA,GACpD,CAAA,KACR,iBAAA,GAAoB,CAAA;EAOZ;;;2BADc,OAAA,CAAQ,IAAA,CAAK,mBAAA,iBAA6B,MAAA,GACxD,CAAA,KACR,mBAAA,GAAsB,CAAA;EAMS;;;4BAAR,OAAA,CAAQ,IAAA,CAAK,oBAAA,iBAA8B,MAAA,GAC1D,CAAA,KACR,oBAAA,GAAuB,CAAA;EAAA;;;;;;;;8DAaR,OAAA,CAAQ,IAAA,CAAK,mBAAA,6BAAyC,MAAA;IAE5D,OAAA,EAAS,CAAA;EAAA,IAAM,CAAA,KACxB,mBAAA,CAAoB,CAAA,IAAK,CAAA;EAUgB;;;;;;;sCAAR,OAAA,CAAQ,IAAA,CAAK,iBAAA,CAAkB,CAAA,kBAAY,MAAA,GACpE,CAAA,KACR,iBAAA,CAAkB,CAAA,IAAK,CAAA;EAMF;;;0BAAA,OAAA,CAAQ,IAAA,CAAK,kBAAA,iBAA4B,MAAA,GACtD,CAAA,KACR,kBAAA,GAAqB,CAAA;AAAA;;;iBCzDV,cAAA,iBAA+B,MAAA,SAAe,aAAA,EAAA,CAAgB,UAAA;EAC5E,SAAA;EACA,KAAA,EAAO,YAAA;EACP,MAAA,EAAQ,CAAA;EACR,KAAA;AAAA,IACE,kBAAA,CAAmB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-DJ3nKzDt.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAgBiB,iBAAA;EAMH;EAJZ,KAAA;EAOiC;EALjC,WAAA;EAK0D;EAH1D,YAAA;AAAA;AAAA,UAGe,iBAAA,SAA0B,iBAAA;EACzC,IAAA;EACA,OAAA;EACA,SAAA;EACA,SAAA;EACA,OAAA,GAAU,MAAA;EAED;EAAT,SAAA;AAAA;AAAA,UAGe,mBAAA,SAA4B,iBAAA;EAC3C,IAAA;EACA,OAAA;EACA,GAAA;EACA,GAAA;EACA,OAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,iBAAA;EAC5C,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,mBAAA,0DACP,iBAAA;EACR,IAAA;EACA,OAAA,EAAS,CAAA;EACT,OAAA,GAAU,CAAA;AAAA;AAAA,UAGK,iBAAA,sBAAuC,iBAAA;EACtD,IAAA;EACA,OAAA,GAAU,CAAA;EAZH;EAcP,MAAA,GAAS,OAAA,CAAQ,CAAA;AAAA;AAAA,UAGF,kBAAA,SAA2B,iBAAA;EAC1C,IAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,KAGU,aAAA,GACR,iBAAA,GACA,mBAAA,GACA,oBAAA,GACA,mBAAA,GACA,iBAAA,GACA,kBAAA;;;;;KAUQ,WAAA,WAAsB,aAAA,IAAiB,CAAA,SAAU,iBAAA,YAEzD,CAAA,SAAU,mBAAA,YAER,CAAA,SAAU,oBAAA,aAER,CAAA,SAAU,mBAAA,YACR,CAAA,WACA,CAAA,SAAU,iBAAA,YACR,CAAA,GACA,CAAA,SAAU,kBAAA;;;;;KAYV,iBAAA,WAA4B,aAAA,IAAiB,CAAA;EAAY,OAAA;AAAA,IACjE,WAAA,CAAY,CAAA,IACZ,WAAA,CAAY,CAAA;AAAA,KAMJ,gBAAA,gBAAgC,MAAA,SAAe,aAAA,mBAC7C,MAAA,GAAS,iBAAA,CAAkB,MAAA,CAAO,CAAA;AAAA,KAOpC,YAAA;;cAGC,eAAA;;cAGA,cAAA;AAAA,UAMI,kBAAA,WACL,MAAA,SAAe,aAAA,IAAiB,MAAA,SAAe,aAAA;EA9EzD;EAiFA,SAAA;EA/EA;EAiFA,KAAA,EAAO,YAAA;EAjFU;EAmFjB,MAAA,EAAQ,CAAA;EAnFU;EAqFlB,KAAA;EAlFkC;;;;;EAwFlC,QAAA;EArFA;;;AAGF;;EAwFE,YAAA;AAAA"}
|