@murumets-ee/settings 0.9.0 → 0.10.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.
@@ -0,0 +1,2 @@
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
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,107 @@
1
+ import { a as InferSettingValue, d as SettingConfig, f as SettingScope, m as SettingsDefinition, o as InferSettingsMap } from "./types-DJ3nKzDt.mjs";
2
+ import { Logger, ToolkitApp } from "@murumets-ee/core";
3
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+
5
+ //#region src/client.d.ts
6
+ interface SettingsClientConfig {
7
+ /** Database client (read-write) */
8
+ db: PostgresJsDatabase;
9
+ /** Logger instance */
10
+ logger?: Logger;
11
+ /** Override scope (defaults to definition's scope) */
12
+ scope?: SettingScope;
13
+ /** Scope ID (required for team/user scope, defaults to '__global__' for global) */
14
+ scopeId?: string;
15
+ }
16
+ interface LocaleOption {
17
+ /** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */
18
+ locale?: string;
19
+ }
20
+ declare class SettingsClient<S extends Record<string, SettingConfig> = Record<string, SettingConfig>> {
21
+ private definition;
22
+ private table;
23
+ private logger?;
24
+ private scope;
25
+ private scopeId;
26
+ private validators;
27
+ constructor(definition: SettingsDefinition<S>, config: SettingsClientConfig);
28
+ /**
29
+ * Get a single setting value.
30
+ *
31
+ * For translatable settings with a locale, tries locale-specific value first,
32
+ * then falls back to the default value, then the schema default, then null.
33
+ */
34
+ get<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<InferSettingValue<S[K]>>;
35
+ /**
36
+ * Get all settings for this namespace/scope as a typed object.
37
+ * Missing values are filled from schema defaults.
38
+ *
39
+ * When locale is specified, translatable settings prefer the locale-specific
40
+ * value over the default value.
41
+ */
42
+ getAll(options?: LocaleOption): Promise<InferSettingsMap<S>>;
43
+ /**
44
+ * Set a single setting value.
45
+ * Validates against the schema before writing.
46
+ *
47
+ * Pass `{ locale }` to write a locale-specific value (only for translatable settings).
48
+ */
49
+ set<K extends string & keyof S>(key: K, value: InferSettingValue<S[K]>, options?: LocaleOption): Promise<void>;
50
+ /**
51
+ * Set multiple settings at once (validated individually).
52
+ * Writes in a transaction — all or nothing.
53
+ *
54
+ * Pass `{ locale }` to write locale-specific values for translatable settings.
55
+ * Non-translatable settings in the values will always write to the default locale.
56
+ */
57
+ setMany(values: Partial<InferSettingsMap<S>>, options?: LocaleOption): Promise<void>;
58
+ /**
59
+ * Delete a setting (resets to default on next get).
60
+ * Pass `{ locale }` to delete only the locale-specific value.
61
+ */
62
+ delete<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<void>;
63
+ /**
64
+ * Check if a setting has a stored value (not relying on default).
65
+ */
66
+ has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean>;
67
+ /**
68
+ * Resolve which locale to use for a given key.
69
+ * Non-translatable settings always use DEFAULT_LOCALE.
70
+ * Translatable settings use the requested locale or DEFAULT_LOCALE.
71
+ */
72
+ private resolveLocale;
73
+ /** Base where conditions: namespace + scope + scopeId. */
74
+ private baseWhere;
75
+ /**
76
+ * Upsert a single setting row via the TableClient.
77
+ * Accepts either the top-level table client or a transactional one —
78
+ * same shape, used inside `setMany`'s transaction.
79
+ */
80
+ private upsertRow;
81
+ }
82
+ //#endregion
83
+ //#region src/client-factory.d.ts
84
+ interface CreateSettingsClientOptions {
85
+ /** Override the toolkit app (defaults to getApp()) */
86
+ app?: ToolkitApp;
87
+ /** Override scope from definition */
88
+ scope?: SettingScope;
89
+ /** Scope ID for team/user scoped settings */
90
+ scopeId?: string;
91
+ }
92
+ /**
93
+ * Create a typed SettingsClient.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * import { createSettingsClient } from '@murumets-ee/settings'
98
+ * import { siteSettings } from './settings/site'
99
+ *
100
+ * const settings = createSettingsClient(siteSettings)
101
+ * const name = await settings.get('siteName') // string
102
+ * ```
103
+ */
104
+ declare function createSettingsClient<S extends Record<string, SettingConfig>>(definition: SettingsDefinition<S>, options?: CreateSettingsClientOptions): SettingsClient<S>;
105
+ //#endregion
106
+ export { SettingsClientConfig as a, SettingsClient as i, createSettingsClient as n, LocaleOption as r, CreateSettingsClientOptions as t };
107
+ //# sourceMappingURL=client-factory-BwJW9_zW.d.mts.map
@@ -0,0 +1 @@
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,4 +1,4 @@
1
- import { c as MediaSettingConfig, d as SettingConfig, f as SettingScope, h as TextSettingConfig, l as NumberSettingConfig, m as SettingsDefinition, n as BooleanSettingConfig, s as JsonSettingConfig, u as SelectSettingConfig } from "./types-D7gEj6e_.mjs";
1
+ import { c as MediaSettingConfig, d as SettingConfig, f as SettingScope, h as TextSettingConfig, l as NumberSettingConfig, m as SettingsDefinition, n as BooleanSettingConfig, s as JsonSettingConfig, u as SelectSettingConfig } from "./types-DJ3nKzDt.mjs";
2
2
 
3
3
  //#region src/builders.d.ts
4
4
  declare const setting: {
@@ -48,4 +48,4 @@ declare function defineSettings<const S extends Record<string, SettingConfig>>(d
48
48
  }): SettingsDefinition<S>;
49
49
  //#endregion
50
50
  export { setting as n, defineSettings as t };
51
- //# sourceMappingURL=define-settings-BwkJo0rZ.d.mts.map
51
+ //# sourceMappingURL=define-settings-YygzoniQ.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-settings-BwkJo0rZ.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
+ {"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"}
package/dist/define.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as SettingScope, h as TextSettingConfig, i as GLOBAL_SCOPE_ID, l as NumberSettingConfig, m as SettingsDefinition, n as BooleanSettingConfig, o as InferSettingsMap, p as SettingToTS, r as DEFAULT_LOCALE, s as JsonSettingConfig, t as BaseSettingConfig, u as SelectSettingConfig } from "./types-D7gEj6e_.mjs";
2
- import { n as setting, t as defineSettings } from "./define-settings-BwkJo0rZ.mjs";
1
+ import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as SettingScope, h as TextSettingConfig, i as GLOBAL_SCOPE_ID, l as NumberSettingConfig, m as SettingsDefinition, n as BooleanSettingConfig, o as InferSettingsMap, p as SettingToTS, r as DEFAULT_LOCALE, s as JsonSettingConfig, t as BaseSettingConfig, u as SelectSettingConfig } from "./types-DJ3nKzDt.mjs";
2
+ import { n as setting, t as defineSettings } from "./define-settings-YygzoniQ.mjs";
3
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 };
package/dist/index.d.mts CHANGED
@@ -1,108 +1,4 @@
1
- import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as SettingScope, h as TextSettingConfig, i as GLOBAL_SCOPE_ID, l as NumberSettingConfig, m as SettingsDefinition, n as BooleanSettingConfig, o as InferSettingsMap, p as SettingToTS, r as DEFAULT_LOCALE, s as JsonSettingConfig, t as BaseSettingConfig, u as SelectSettingConfig } from "./types-D7gEj6e_.mjs";
2
- import { n as setting, t as defineSettings } from "./define-settings-BwkJo0rZ.mjs";
3
- import { Logger, ToolkitApp } from "@murumets-ee/core";
4
- import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
5
-
6
- //#region src/client.d.ts
7
- interface SettingsClientConfig {
8
- /** Database client (read-write) */
9
- db: PostgresJsDatabase;
10
- /** Logger instance */
11
- logger?: Logger;
12
- /** Override scope (defaults to definition's scope) */
13
- scope?: SettingScope;
14
- /** Scope ID (required for team/user scope, defaults to '__global__' for global) */
15
- scopeId?: string;
16
- }
17
- interface LocaleOption {
18
- /** Locale code for translatable settings (e.g. 'et', 'ru'). Ignored for non-translatable settings. */
19
- locale?: string;
20
- }
21
- declare class SettingsClient<S extends Record<string, SettingConfig> = Record<string, SettingConfig>> {
22
- private definition;
23
- private table;
24
- private logger?;
25
- private scope;
26
- private scopeId;
27
- private validators;
28
- constructor(definition: SettingsDefinition<S>, config: SettingsClientConfig);
29
- /**
30
- * Get a single setting value.
31
- *
32
- * For translatable settings with a locale, tries locale-specific value first,
33
- * then falls back to the default value, then the schema default, then null.
34
- */
35
- get<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<InferSettingValue<S[K]>>;
36
- /**
37
- * Get all settings for this namespace/scope as a typed object.
38
- * Missing values are filled from schema defaults.
39
- *
40
- * When locale is specified, translatable settings prefer the locale-specific
41
- * value over the default value.
42
- */
43
- getAll(options?: LocaleOption): Promise<InferSettingsMap<S>>;
44
- /**
45
- * Set a single setting value.
46
- * Validates against the schema before writing.
47
- *
48
- * Pass `{ locale }` to write a locale-specific value (only for translatable settings).
49
- */
50
- set<K extends string & keyof S>(key: K, value: InferSettingValue<S[K]>, options?: LocaleOption): Promise<void>;
51
- /**
52
- * Set multiple settings at once (validated individually).
53
- * Writes in a transaction — all or nothing.
54
- *
55
- * Pass `{ locale }` to write locale-specific values for translatable settings.
56
- * Non-translatable settings in the values will always write to the default locale.
57
- */
58
- setMany(values: Partial<InferSettingsMap<S>>, options?: LocaleOption): Promise<void>;
59
- /**
60
- * Delete a setting (resets to default on next get).
61
- * Pass `{ locale }` to delete only the locale-specific value.
62
- */
63
- delete<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<void>;
64
- /**
65
- * Check if a setting has a stored value (not relying on default).
66
- */
67
- has<K extends string & keyof S>(key: K, options?: LocaleOption): Promise<boolean>;
68
- /**
69
- * Resolve which locale to use for a given key.
70
- * Non-translatable settings always use DEFAULT_LOCALE.
71
- * Translatable settings use the requested locale or DEFAULT_LOCALE.
72
- */
73
- private resolveLocale;
74
- /** Base where conditions: namespace + scope + scopeId. */
75
- private baseWhere;
76
- /**
77
- * Upsert a single setting row via the TableClient.
78
- * Accepts either the top-level table client or a transactional one —
79
- * same shape, used inside `setMany`'s transaction.
80
- */
81
- private upsertRow;
82
- }
83
- //#endregion
84
- //#region src/client-factory.d.ts
85
- interface CreateSettingsClientOptions {
86
- /** Override the toolkit app (defaults to getApp()) */
87
- app?: ToolkitApp;
88
- /** Override scope from definition */
89
- scope?: SettingScope;
90
- /** Scope ID for team/user scoped settings */
91
- scopeId?: string;
92
- }
93
- /**
94
- * Create a typed SettingsClient.
95
- *
96
- * @example
97
- * ```typescript
98
- * import { createSettingsClient } from '@murumets-ee/settings'
99
- * import { siteSettings } from './settings/site'
100
- *
101
- * const settings = createSettingsClient(siteSettings)
102
- * const name = await settings.get('siteName') // string
103
- * ```
104
- */
105
- declare function createSettingsClient<S extends Record<string, SettingConfig>>(definition: SettingsDefinition<S>, options?: CreateSettingsClientOptions): SettingsClient<S>;
106
- //#endregion
107
- 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 };
108
- //# sourceMappingURL=index.d.mts.map
1
+ import { a as InferSettingValue, c as MediaSettingConfig, d as SettingConfig, f as SettingScope, h as TextSettingConfig, i as GLOBAL_SCOPE_ID, l as NumberSettingConfig, m as SettingsDefinition, n as BooleanSettingConfig, o as InferSettingsMap, p as SettingToTS, r as DEFAULT_LOCALE, s as JsonSettingConfig, t as BaseSettingConfig, u as SelectSettingConfig } from "./types-DJ3nKzDt.mjs";
2
+ import { n as setting, t as defineSettings } from "./define-settings-YygzoniQ.mjs";
3
+ import { a as SettingsClientConfig, i as SettingsClient, n as createSettingsClient, r as LocaleOption, t as CreateSettingsClientOptions } from "./client-factory-BwJW9_zW.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 SettingScope, type SettingToTS, SettingsClient, type SettingsClientConfig, type SettingsDefinition, type TextSettingConfig, createSettingsClient, defineSettings, setting };
package/dist/index.mjs CHANGED
@@ -1,2 +1 @@
1
- import{i as e,n as t,r as n,t as r}from"./define-settings-Bi3STtAH.mjs";import{toolkitSettingsTable as i}from"./schema.mjs";import"server-only";import{z as a}from"zod";import{getApp as o}from"@murumets-ee/core";function s(e){switch(e.type){case`text`:{let t=a.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=a.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 a.boolean();case`select`:return a.enum(e.options);case`json`:return e.schema??a.unknown();case`media`:return a.string().uuid();default:return a.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=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=c(e)}async get(e,n){let r=this.definition.schema[e],i=r?.translatable&&n?.locale?n.locale:null;if(this.logger?.debug({namespace:this.definition.namespace,key:e,locale:i},`Getting setting`),i){let n=await this.table.findMany({where:{...this.baseWhere(),key:e,$or:[{locale:i},{locale:t}]},limit:2}),r=n.find(e=>e.locale===i),a=n.find(e=>e.locale===t),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:e,locale:t},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(e){let n=e?.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:t},{locale:n}]},limit:1e3}):await this.table.findMany({where:{...this.baseWhere(),locale:t},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(e,n){return this.definition.schema[e]?.translatable&&n?.locale?n.locale:t}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 u(e,t){let n=t?.app??o();return new l(e,{db:n.db.readWrite,logger:n.logger.child({settings:e.namespace}),scope:t?.scope,scopeId:t?.scopeId})}export{t as DEFAULT_LOCALE,n as GLOBAL_SCOPE_ID,l as SettingsClient,u as createSettingsClient,r as defineSettings,e as setting};
2
- //# sourceMappingURL=index.mjs.map
1
+ import{i as e,n as t,r as n,t as r}from"./define-settings-Bi3STtAH.mjs";import{n as i,t as a}from"./client-factory-BLDPF2zz.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,4 +1,4 @@
1
- import { m as SettingsDefinition } from "./types-D7gEj6e_.mjs";
1
+ import { m as SettingsDefinition } from "./types-DJ3nKzDt.mjs";
2
2
  import { Plugin } from "@murumets-ee/core";
3
3
 
4
4
  //#region src/plugin.d.ts
@@ -0,0 +1,3 @@
1
+ import { n as setting, t as defineSettings } from "./define-settings-YygzoniQ.mjs";
2
+ import { a as SettingsClientConfig, i as SettingsClient, n as createSettingsClient, r as LocaleOption, t as CreateSettingsClientOptions } from "./client-factory-BwJW9_zW.mjs";
3
+ export { type CreateSettingsClientOptions, type LocaleOption, SettingsClient, type SettingsClientConfig, createSettingsClient, defineSettings, setting };
@@ -0,0 +1 @@
1
+ import{i as e,t}from"./define-settings-Bi3STtAH.mjs";import{n,t as r}from"./client-factory-BLDPF2zz.mjs";export{n as SettingsClient,r as createSettingsClient,t as defineSettings,e as setting};
@@ -88,4 +88,4 @@ interface SettingsDefinition<S extends Record<string, SettingConfig> = Record<st
88
88
  }
89
89
  //#endregion
90
90
  export { InferSettingValue as a, MediaSettingConfig as c, SettingConfig as d, SettingScope as f, TextSettingConfig as h, GLOBAL_SCOPE_ID as i, NumberSettingConfig as l, SettingsDefinition as m, BooleanSettingConfig as n, InferSettingsMap as o, SettingToTS as p, DEFAULT_LOCALE as r, JsonSettingConfig as s, BaseSettingConfig as t, SelectSettingConfig as u };
91
- //# sourceMappingURL=types-D7gEj6e_.d.mts.map
91
+ //# sourceMappingURL=types-DJ3nKzDt.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-D7gEj6e_.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"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/settings",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,6 +12,10 @@
12
12
  "types": "./dist/define.d.mts",
13
13
  "import": "./dist/define.mjs"
14
14
  },
15
+ "./runtime": {
16
+ "types": "./dist/runtime.d.mts",
17
+ "import": "./dist/runtime.mjs"
18
+ },
15
19
  "./schema": {
16
20
  "types": "./dist/schema.d.mts",
17
21
  "import": "./dist/schema.mjs"
@@ -36,9 +40,9 @@
36
40
  "drizzle-orm": "^0.45.2",
37
41
  "zod": "^3.24.1",
38
42
  "server-only": "^0.0.1",
39
- "@murumets-ee/core": "0.9.0",
40
- "@murumets-ee/db": "0.9.0",
41
- "@murumets-ee/logging": "0.9.0"
43
+ "@murumets-ee/core": "0.10.0",
44
+ "@murumets-ee/db": "0.10.0",
45
+ "@murumets-ee/logging": "0.10.0"
42
46
  },
43
47
  "devDependencies": {
44
48
  "@types/node": "^22.10.5",
@@ -46,6 +50,9 @@
46
50
  "typescript": "^5.7.3",
47
51
  "vitest": "^2.1.8"
48
52
  },
53
+ "typeCoverage": {
54
+ "atLeast": 99.74
55
+ },
49
56
  "scripts": {
50
57
  "build": "tsdown",
51
58
  "dev": "tsdown --watch",
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/client-factory.ts"],"mappings":";;;;;;UA2CiB,oBAAA;EAwBU;EAtBzB,EAAA,EAAI,kBAAA;EAsBqD;EApBzD,MAAA,GAAS,MAAA;EA6BkC;EA3B3C,KAAA,GAAQ,YAAA;EA2B+C;EAzBvD,OAAA;AAAA;AAAA,UAGe,YAAA;EAmDc;EAjD7B,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;EAkJ1B;;;;;;EAxHvB,GAAA,0BAA6B,CAAA,CAAA,CACjC,GAAA,EAAK,CAAA,EACL,OAAA,GAAU,YAAA,GACT,OAAA,CAAQ,iBAAA,CAAkB,CAAA,CAAE,CAAA;EA+I+B;;;;;;;EA7FxD,MAAA,CAAO,OAAA,GAAU,YAAA,GAAe,OAAA,CAAQ,gBAAA,CAAiB,CAAA;EA0IP;;;;;;EAzElD,GAAA,0BAA6B,CAAA,CAAA,CACjC,GAAA,EAAK,CAAA,EACL,KAAA,EAAO,iBAAA,CAAkB,CAAA,CAAE,CAAA,IAC3B,OAAA,GAAU,YAAA,GACT,OAAA;EA7JuC;;;;;;;EAqLpC,OAAA,CAAQ,MAAA,EAAQ,OAAA,CAAQ,gBAAA,CAAiB,CAAA,IAAK,OAAA,GAAU,YAAA,GAAe,OAAA;;;;;EA8BvE,MAAA,0BAAgC,CAAA,CAAA,CAAG,GAAA,EAAK,CAAA,EAAG,OAAA,GAAU,YAAA,GAAe,OAAA;EA1M3B;;;EAyNzC,GAAA,0BAA6B,CAAA,CAAA,CAAG,GAAA,EAAK,CAAA,EAAG,OAAA,GAAU,YAAA,GAAe,OAAA;EA9LhE;;;;;EAAA,QAiNC,aAAA;EA/MqB;EAAA,QAwNrB,SAAA;EAtKF;;;;;EAAA,QAmLQ,SAAA;AAAA;;;UCnUC,2BAAA;;EAEf,GAAA,GAAM,UAAA;EDkCG;EChCT,KAAA,GAAQ,YAAA;EDkCY;EChCpB,OAAA;AAAA;;;;;;;;;ADqCF;;;;iBCtBgB,oBAAA,WAA+B,MAAA,SAAe,aAAA,EAAA,CAC5D,UAAA,EAAY,kBAAA,CAAmB,CAAA,GAC/B,OAAA,GAAU,2BAAA,GACT,cAAA,CAAe,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.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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"}