@murumets-ee/settings 0.1.5 → 0.1.6

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,318 @@
1
+ import * as _$drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
2
+
3
+ //#region src/schema.d.ts
4
+ /**
5
+ * Drizzle schema for settings tables.
6
+ *
7
+ * Two tables:
8
+ * - toolkit_settings: typed key-value settings grouped by namespace and scope
9
+ * - toolkit_view_state: schemaless user-scoped JSON blobs with optional TTL
10
+ *
11
+ * Usage in drizzle.config.ts:
12
+ * ```typescript
13
+ * import type { Config } from 'drizzle-kit'
14
+ * export default {
15
+ * schema: ['./generated/schema.ts', '@murumets-ee/settings/schema'],
16
+ * // ...
17
+ * } satisfies Config
18
+ * ```
19
+ */
20
+ /**
21
+ * Typed settings table.
22
+ *
23
+ * Stores key-value pairs grouped by namespace and scoped
24
+ * to global, team, or user contexts.
25
+ *
26
+ * scopeId uses '__global__' sentinel for global scope to avoid
27
+ * PostgreSQL's NULL != NULL behavior in unique constraints.
28
+ */
29
+ declare const toolkitSettings: _$drizzle_orm_pg_core0.PgTableWithColumns<{
30
+ name: "toolkit_settings";
31
+ schema: undefined;
32
+ columns: {
33
+ id: _$drizzle_orm_pg_core0.PgColumn<{
34
+ name: "id";
35
+ tableName: "toolkit_settings";
36
+ dataType: "string";
37
+ columnType: "PgUUID";
38
+ data: string;
39
+ driverParam: string;
40
+ notNull: true;
41
+ hasDefault: true;
42
+ isPrimaryKey: true;
43
+ isAutoincrement: false;
44
+ hasRuntimeDefault: false;
45
+ enumValues: undefined;
46
+ baseColumn: never;
47
+ identity: undefined;
48
+ generated: undefined;
49
+ }, {}, {}>;
50
+ namespace: _$drizzle_orm_pg_core0.PgColumn<{
51
+ name: "namespace";
52
+ tableName: "toolkit_settings";
53
+ dataType: "string";
54
+ columnType: "PgVarchar";
55
+ data: string;
56
+ driverParam: string;
57
+ notNull: true;
58
+ hasDefault: false;
59
+ isPrimaryKey: false;
60
+ isAutoincrement: false;
61
+ hasRuntimeDefault: false;
62
+ enumValues: [string, ...string[]];
63
+ baseColumn: never;
64
+ identity: undefined;
65
+ generated: undefined;
66
+ }, {}, {
67
+ length: 100;
68
+ }>;
69
+ scope: _$drizzle_orm_pg_core0.PgColumn<{
70
+ name: "scope";
71
+ tableName: "toolkit_settings";
72
+ dataType: "string";
73
+ columnType: "PgVarchar";
74
+ data: string;
75
+ driverParam: string;
76
+ notNull: true;
77
+ hasDefault: true;
78
+ isPrimaryKey: false;
79
+ isAutoincrement: false;
80
+ hasRuntimeDefault: false;
81
+ enumValues: [string, ...string[]];
82
+ baseColumn: never;
83
+ identity: undefined;
84
+ generated: undefined;
85
+ }, {}, {
86
+ length: 20;
87
+ }>;
88
+ scopeId: _$drizzle_orm_pg_core0.PgColumn<{
89
+ name: "scope_id";
90
+ tableName: "toolkit_settings";
91
+ dataType: "string";
92
+ columnType: "PgVarchar";
93
+ data: string;
94
+ driverParam: string;
95
+ notNull: true;
96
+ hasDefault: true;
97
+ isPrimaryKey: false;
98
+ isAutoincrement: false;
99
+ hasRuntimeDefault: false;
100
+ enumValues: [string, ...string[]];
101
+ baseColumn: never;
102
+ identity: undefined;
103
+ generated: undefined;
104
+ }, {}, {
105
+ length: 100;
106
+ }>;
107
+ key: _$drizzle_orm_pg_core0.PgColumn<{
108
+ name: "key";
109
+ tableName: "toolkit_settings";
110
+ dataType: "string";
111
+ columnType: "PgVarchar";
112
+ data: string;
113
+ driverParam: string;
114
+ notNull: true;
115
+ hasDefault: false;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: [string, ...string[]];
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {
124
+ length: 255;
125
+ }>;
126
+ locale: _$drizzle_orm_pg_core0.PgColumn<{
127
+ name: "locale";
128
+ tableName: "toolkit_settings";
129
+ dataType: "string";
130
+ columnType: "PgVarchar";
131
+ data: string;
132
+ driverParam: string;
133
+ notNull: true;
134
+ hasDefault: true;
135
+ isPrimaryKey: false;
136
+ isAutoincrement: false;
137
+ hasRuntimeDefault: false;
138
+ enumValues: [string, ...string[]];
139
+ baseColumn: never;
140
+ identity: undefined;
141
+ generated: undefined;
142
+ }, {}, {
143
+ length: 10;
144
+ }>;
145
+ value: _$drizzle_orm_pg_core0.PgColumn<{
146
+ name: "value";
147
+ tableName: "toolkit_settings";
148
+ dataType: "json";
149
+ columnType: "PgJsonb";
150
+ data: unknown;
151
+ driverParam: unknown;
152
+ notNull: false;
153
+ hasDefault: false;
154
+ isPrimaryKey: false;
155
+ isAutoincrement: false;
156
+ hasRuntimeDefault: false;
157
+ enumValues: undefined;
158
+ baseColumn: never;
159
+ identity: undefined;
160
+ generated: undefined;
161
+ }, {}, {}>;
162
+ updatedAt: _$drizzle_orm_pg_core0.PgColumn<{
163
+ name: "updated_at";
164
+ tableName: "toolkit_settings";
165
+ dataType: "date";
166
+ columnType: "PgTimestamp";
167
+ data: Date;
168
+ driverParam: string;
169
+ notNull: true;
170
+ hasDefault: true;
171
+ isPrimaryKey: false;
172
+ isAutoincrement: false;
173
+ hasRuntimeDefault: false;
174
+ enumValues: undefined;
175
+ baseColumn: never;
176
+ identity: undefined;
177
+ generated: undefined;
178
+ }, {}, {}>;
179
+ updatedBy: _$drizzle_orm_pg_core0.PgColumn<{
180
+ name: "updated_by";
181
+ tableName: "toolkit_settings";
182
+ dataType: "string";
183
+ columnType: "PgUUID";
184
+ data: string;
185
+ driverParam: string;
186
+ notNull: false;
187
+ hasDefault: false;
188
+ isPrimaryKey: false;
189
+ isAutoincrement: false;
190
+ hasRuntimeDefault: false;
191
+ enumValues: undefined;
192
+ baseColumn: never;
193
+ identity: undefined;
194
+ generated: undefined;
195
+ }, {}, {}>;
196
+ };
197
+ dialect: "pg";
198
+ }>;
199
+ /**
200
+ * View state table.
201
+ *
202
+ * Stores schemaless user-scoped JSON blobs for persisting
203
+ * UI state (table filters, column order, etc.) with optional TTL.
204
+ */
205
+ declare const toolkitViewState: _$drizzle_orm_pg_core0.PgTableWithColumns<{
206
+ name: "toolkit_view_state";
207
+ schema: undefined;
208
+ columns: {
209
+ id: _$drizzle_orm_pg_core0.PgColumn<{
210
+ name: "id";
211
+ tableName: "toolkit_view_state";
212
+ dataType: "string";
213
+ columnType: "PgUUID";
214
+ data: string;
215
+ driverParam: string;
216
+ notNull: true;
217
+ hasDefault: true;
218
+ isPrimaryKey: true;
219
+ isAutoincrement: false;
220
+ hasRuntimeDefault: false;
221
+ enumValues: undefined;
222
+ baseColumn: never;
223
+ identity: undefined;
224
+ generated: undefined;
225
+ }, {}, {}>;
226
+ userId: _$drizzle_orm_pg_core0.PgColumn<{
227
+ name: "user_id";
228
+ tableName: "toolkit_view_state";
229
+ dataType: "string";
230
+ columnType: "PgUUID";
231
+ data: string;
232
+ driverParam: string;
233
+ notNull: true;
234
+ hasDefault: false;
235
+ isPrimaryKey: false;
236
+ isAutoincrement: false;
237
+ hasRuntimeDefault: false;
238
+ enumValues: undefined;
239
+ baseColumn: never;
240
+ identity: undefined;
241
+ generated: undefined;
242
+ }, {}, {}>;
243
+ viewKey: _$drizzle_orm_pg_core0.PgColumn<{
244
+ name: "view_key";
245
+ tableName: "toolkit_view_state";
246
+ dataType: "string";
247
+ columnType: "PgVarchar";
248
+ data: string;
249
+ driverParam: string;
250
+ notNull: true;
251
+ hasDefault: false;
252
+ isPrimaryKey: false;
253
+ isAutoincrement: false;
254
+ hasRuntimeDefault: false;
255
+ enumValues: [string, ...string[]];
256
+ baseColumn: never;
257
+ identity: undefined;
258
+ generated: undefined;
259
+ }, {}, {
260
+ length: 255;
261
+ }>;
262
+ state: _$drizzle_orm_pg_core0.PgColumn<{
263
+ name: "state";
264
+ tableName: "toolkit_view_state";
265
+ dataType: "json";
266
+ columnType: "PgJsonb";
267
+ data: unknown;
268
+ driverParam: unknown;
269
+ notNull: true;
270
+ hasDefault: false;
271
+ isPrimaryKey: false;
272
+ isAutoincrement: false;
273
+ hasRuntimeDefault: false;
274
+ enumValues: undefined;
275
+ baseColumn: never;
276
+ identity: undefined;
277
+ generated: undefined;
278
+ }, {}, {}>;
279
+ expiresAt: _$drizzle_orm_pg_core0.PgColumn<{
280
+ name: "expires_at";
281
+ tableName: "toolkit_view_state";
282
+ dataType: "date";
283
+ columnType: "PgTimestamp";
284
+ data: Date;
285
+ driverParam: string;
286
+ notNull: false;
287
+ hasDefault: false;
288
+ isPrimaryKey: false;
289
+ isAutoincrement: false;
290
+ hasRuntimeDefault: false;
291
+ enumValues: undefined;
292
+ baseColumn: never;
293
+ identity: undefined;
294
+ generated: undefined;
295
+ }, {}, {}>;
296
+ updatedAt: _$drizzle_orm_pg_core0.PgColumn<{
297
+ name: "updated_at";
298
+ tableName: "toolkit_view_state";
299
+ dataType: "date";
300
+ columnType: "PgTimestamp";
301
+ data: Date;
302
+ driverParam: string;
303
+ notNull: true;
304
+ hasDefault: true;
305
+ isPrimaryKey: false;
306
+ isAutoincrement: false;
307
+ hasRuntimeDefault: false;
308
+ enumValues: undefined;
309
+ baseColumn: never;
310
+ identity: undefined;
311
+ generated: undefined;
312
+ }, {}, {}>;
313
+ };
314
+ dialect: "pg";
315
+ }>;
316
+ //#endregion
317
+ export { toolkitSettings, toolkitViewState };
318
+ //# sourceMappingURL=schema.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.mts","names":[],"sources":["../src/schema.ts"],"mappings":";;;;;;AA4BA;;;;;;;;;;;;;;;;;;;;;;cAAa,eAAA,yBAAe,kBAAA;;;;QAsB3B,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAQY,gBAAA,yBAAgB,kBAAA;;;;QAa5B,sBAAA,CAAA,QAAA"}
@@ -0,0 +1,2 @@
1
+ import{jsonb as e,pgTable as t,timestamp as n,unique as r,uuid as i,varchar as a}from"drizzle-orm/pg-core";const o=t(`toolkit_settings`,{id:i(`id`).primaryKey().defaultRandom(),namespace:a(`namespace`,{length:100}).notNull(),scope:a(`scope`,{length:20}).notNull().default(`global`),scopeId:a(`scope_id`,{length:100}).notNull().default(`__global__`),key:a(`key`,{length:255}).notNull(),locale:a(`locale`,{length:10}).notNull().default(`_default`),value:e(`value`),updatedAt:n(`updated_at`,{withTimezone:!0}).notNull().defaultNow(),updatedBy:i(`updated_by`)},e=>({uniqueSetting:r().on(e.namespace,e.scope,e.scopeId,e.key,e.locale)})),s=t(`toolkit_view_state`,{id:i(`id`).primaryKey().defaultRandom(),userId:i(`user_id`).notNull(),viewKey:a(`view_key`,{length:255}).notNull(),state:e(`state`).notNull(),expiresAt:n(`expires_at`,{withTimezone:!0}),updatedAt:n(`updated_at`,{withTimezone:!0}).notNull().defaultNow()},e=>({uniqueUserView:r().on(e.userId,e.viewKey)}));export{o as toolkitSettings,s as toolkitViewState};
2
+ //# sourceMappingURL=schema.mjs.map
@@ -0,0 +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 * Usage in drizzle.config.ts:\n * ```typescript\n * import type { Config } from 'drizzle-kit'\n * export default {\n * schema: ['./generated/schema.ts', '@murumets-ee/settings/schema'],\n * // ...\n * } satisfies Config\n * ```\n */\n\nimport { jsonb, pgTable, timestamp, unique, uuid, varchar } from 'drizzle-orm/pg-core'\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 toolkitSettings = pgTable(\n 'toolkit_settings',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n namespace: varchar('namespace', { length: 100 }).notNull(),\n scope: varchar('scope', { length: 20 }).notNull().default('global'),\n scopeId: varchar('scope_id', { length: 100 }).notNull().default('__global__'),\n key: varchar('key', { length: 255 }).notNull(),\n locale: varchar('locale', { length: 10 }).notNull().default('_default'),\n value: jsonb('value'),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n updatedBy: uuid('updated_by'),\n },\n (table) => ({\n uniqueSetting: unique().on(\n table.namespace,\n table.scope,\n table.scopeId,\n table.key,\n table.locale,\n ),\n }),\n)\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 toolkitViewState = pgTable(\n 'toolkit_view_state',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n userId: uuid('user_id').notNull(),\n viewKey: varchar('view_key', { length: 255 }).notNull(),\n state: jsonb('state').notNull(),\n expiresAt: timestamp('expires_at', { withTimezone: true }),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => ({\n uniqueUserView: unique().on(table.userId, table.viewKey),\n }),\n)\n"],"mappings":"2GA4BA,MAAa,EAAkB,EAC7B,mBACA,CACE,GAAI,EAAK,KAAK,CAAC,YAAY,CAAC,eAAe,CAC3C,UAAW,EAAQ,YAAa,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CAC1D,MAAO,EAAQ,QAAS,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,SAAS,CACnE,QAAS,EAAQ,WAAY,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CAAC,QAAQ,aAAa,CAC7E,IAAK,EAAQ,MAAO,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CAC9C,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,WAAW,CACvE,MAAO,EAAM,QAAQ,CACrB,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CACjF,UAAW,EAAK,aAAa,CAC9B,CACA,IAAW,CACV,cAAe,GAAQ,CAAC,GACtB,EAAM,UACN,EAAM,MACN,EAAM,QACN,EAAM,IACN,EAAM,OACP,CACF,EACF,CAQY,EAAmB,EAC9B,qBACA,CACE,GAAI,EAAK,KAAK,CAAC,YAAY,CAAC,eAAe,CAC3C,OAAQ,EAAK,UAAU,CAAC,SAAS,CACjC,QAAS,EAAQ,WAAY,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CACvD,MAAO,EAAM,QAAQ,CAAC,SAAS,CAC/B,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAC1D,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CAClF,CACA,IAAW,CACV,eAAgB,GAAQ,CAAC,GAAG,EAAM,OAAQ,EAAM,QAAQ,CACzD,EACF"}
@@ -0,0 +1,48 @@
1
+ import { Logger } from "@murumets-ee/core";
2
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+
4
+ //#region src/view-state.d.ts
5
+ interface ViewStateClientConfig {
6
+ /** Database client (read-write) */
7
+ db: PostgresJsDatabase;
8
+ /** User ID for scoping */
9
+ userId: string;
10
+ /** Logger instance */
11
+ logger?: Logger;
12
+ /** Default TTL in seconds for view state entries. Defaults to 30 days. */
13
+ defaultTtl?: number;
14
+ }
15
+ declare class ViewStateClient {
16
+ private db;
17
+ private userId;
18
+ private logger?;
19
+ private defaultTtl;
20
+ constructor(config: ViewStateClientConfig);
21
+ /**
22
+ * Save view state (upsert).
23
+ */
24
+ save<T extends Record<string, unknown>>(viewKey: string, state: T, options?: {
25
+ ttl?: number;
26
+ }): Promise<void>;
27
+ /**
28
+ * Load view state. Returns null if not found or expired.
29
+ */
30
+ load<T extends Record<string, unknown> = Record<string, unknown>>(viewKey: string): Promise<T | null>;
31
+ /**
32
+ * Clear a specific view state entry.
33
+ */
34
+ clear(viewKey: string): Promise<void>;
35
+ /**
36
+ * Clear all expired view state entries (maintenance task).
37
+ * Call periodically (e.g., from a cron job or queue task).
38
+ * Returns the number of entries removed.
39
+ */
40
+ clearExpired(): Promise<number>;
41
+ }
42
+ /**
43
+ * Factory function following toolkit conventions.
44
+ */
45
+ declare function createViewStateClient(config: ViewStateClientConfig): ViewStateClient;
46
+ //#endregion
47
+ export { ViewStateClient, ViewStateClientConfig, createViewStateClient };
48
+ //# sourceMappingURL=view-state.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-state.d.mts","names":[],"sources":["../src/view-state.ts"],"mappings":";;;;UA2BiB,qBAAA;EAkEgC;EAhE/C,EAAA,EAAI,kBAAA;EAkED;EAhEH,MAAA;EAsGsB;EApGtB,MAAA,GAAS,MAAA;EAoGoB;EAlG7B,UAAA;AAAA;AAAA,cAMW,eAAA;EAAA,QACH,EAAA;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;;;EA6BI,IAAA,WAAe,MAAA,oBAA0B,MAAA,kBAAA,CAC7C,OAAA,WACC,OAAA,CAAQ,CAAA;EAFU;;;EA2Bf,KAAA,CAAM,OAAA,WAAkB,OAAA;EAzBnB;;;;;EAsCL,YAAA,CAAA,GAAgB,OAAA;AAAA;;AAiBxB;;iBAAgB,qBAAA,CAAsB,MAAA,EAAQ,qBAAA,GAAwB,eAAA"}
@@ -0,0 +1,2 @@
1
+ import{toolkitViewState as e}from"./schema.mjs";import{and as t,eq as n,lt as r}from"drizzle-orm";var i=class{db;userId;logger;defaultTtl;constructor(e){if(typeof window<`u`)throw Error(`ViewStateClient cannot be used in browser code.`);this.db=e.db,this.userId=e.userId,this.logger=e.logger,this.defaultTtl=e.defaultTtl??2592e3}async save(t,n,r){this.logger?.debug({userId:this.userId,viewKey:t},`Saving view state`);let i=r?.ttl??this.defaultTtl,a=new Date(Date.now()+i*1e3);await this.db.insert(e).values({userId:this.userId,viewKey:t,state:n,expiresAt:a,updatedAt:new Date}).onConflictDoUpdate({target:[e.userId,e.viewKey],set:{state:n,expiresAt:a,updatedAt:new Date}})}async load(r){this.logger?.debug({userId:this.userId,viewKey:r},`Loading view state`);let i=await this.db.select({state:e.state,expiresAt:e.expiresAt}).from(e).where(t(n(e.userId,this.userId),n(e.viewKey,r))).limit(1);if(i.length===0)return null;let a=i[0];return a.expiresAt&&a.expiresAt<new Date?(await this.clear(r),null):a.state}async clear(r){this.logger?.debug({userId:this.userId,viewKey:r},`Clearing view state`),await this.db.delete(e).where(t(n(e.userId,this.userId),n(e.viewKey,r)))}async clearExpired(){this.logger?.info(`Clearing expired view state entries`);let t=(await this.db.delete(e).where(r(e.expiresAt,new Date)).returning({id:e.id})).length;return this.logger?.info({count:t},`Expired view state entries cleared`),t}};function a(e){return new i(e)}export{i as ViewStateClient,a as createViewStateClient};
2
+ //# sourceMappingURL=view-state.mjs.map
@@ -0,0 +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 { and, eq, lt } from 'drizzle-orm'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { toolkitViewState } from './schema.js'\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\n /** Default TTL in seconds for view state entries. Defaults to 30 days. */\n defaultTtl?: number\n}\n\n/** 30 days in seconds */\nconst DEFAULT_TTL = 30 * 24 * 60 * 60\n\nexport class ViewStateClient {\n private db: PostgresJsDatabase\n private userId: string\n private logger?: Logger\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.db = 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.db\n .insert(toolkitViewState)\n .values({\n userId: this.userId,\n viewKey,\n state,\n expiresAt,\n updatedAt: new Date(),\n })\n .onConflictDoUpdate({\n target: [toolkitViewState.userId, toolkitViewState.viewKey],\n set: {\n state,\n expiresAt,\n 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 rows = await this.db\n .select({ state: toolkitViewState.state, expiresAt: toolkitViewState.expiresAt })\n .from(toolkitViewState)\n .where(and(eq(toolkitViewState.userId, this.userId), eq(toolkitViewState.viewKey, viewKey)))\n .limit(1)\n\n if (rows.length === 0) return null\n\n const row = rows[0]\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.db\n .delete(toolkitViewState)\n .where(and(eq(toolkitViewState.userId, this.userId), eq(toolkitViewState.viewKey, 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 result = await this.db\n .delete(toolkitViewState)\n .where(lt(toolkitViewState.expiresAt, new Date()))\n .returning({ id: toolkitViewState.id })\n\n const count = result.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":"kGAyCA,IAAa,EAAb,KAA6B,CAC3B,GACA,OACA,OACA,WAEA,YAAY,EAA+B,CACzC,GAAI,OAAO,OAAW,IACpB,MAAU,MAAM,kDAAkD,CAGpE,KAAK,GAAK,EAAO,GACjB,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,GACR,OAAO,EAAiB,CACxB,OAAO,CACN,OAAQ,KAAK,OACb,UACA,QACA,YACA,UAAW,IAAI,KAChB,CAAC,CACD,mBAAmB,CAClB,OAAQ,CAAC,EAAiB,OAAQ,EAAiB,QAAQ,CAC3D,IAAK,CACH,QACA,YACA,UAAW,IAAI,KAChB,CACF,CAAC,CAMN,MAAM,KACJ,EACmB,CACnB,KAAK,QAAQ,MAAM,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAE,qBAAqB,CAE1E,IAAM,EAAO,MAAM,KAAK,GACrB,OAAO,CAAE,MAAO,EAAiB,MAAO,UAAW,EAAiB,UAAW,CAAC,CAChF,KAAK,EAAiB,CACtB,MAAM,EAAI,EAAG,EAAiB,OAAQ,KAAK,OAAO,CAAE,EAAG,EAAiB,QAAS,EAAQ,CAAC,CAAC,CAC3F,MAAM,EAAE,CAEX,GAAI,EAAK,SAAW,EAAG,OAAO,KAE9B,IAAM,EAAM,EAAK,GAQjB,OALI,EAAI,WAAa,EAAI,UAAY,IAAI,MACvC,MAAM,KAAK,MAAM,EAAQ,CAClB,MAGF,EAAI,MAMb,MAAM,MAAM,EAAgC,CAC1C,KAAK,QAAQ,MAAM,CAAE,OAAQ,KAAK,OAAQ,UAAS,CAAE,sBAAsB,CAE3E,MAAM,KAAK,GACR,OAAO,EAAiB,CACxB,MAAM,EAAI,EAAG,EAAiB,OAAQ,KAAK,OAAO,CAAE,EAAG,EAAiB,QAAS,EAAQ,CAAC,CAAC,CAQhG,MAAM,cAAgC,CACpC,KAAK,QAAQ,KAAK,sCAAsC,CAOxD,IAAM,GALS,MAAM,KAAK,GACvB,OAAO,EAAiB,CACxB,MAAM,EAAG,EAAiB,UAAW,IAAI,KAAO,CAAC,CACjD,UAAU,CAAE,GAAI,EAAiB,GAAI,CAAC,EAEpB,OAErB,OADA,KAAK,QAAQ,KAAK,CAAE,QAAO,CAAE,qCAAqC,CAC3D,IAOX,SAAgB,EAAsB,EAAgD,CACpF,OAAO,IAAI,EAAgB,EAAO"}
package/package.json CHANGED
@@ -1,28 +1,28 @@
1
1
  {
2
2
  "name": "@murumets-ee/settings",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
8
+ "types": "./dist/index.d.mts",
9
+ "import": "./dist/index.mjs"
10
10
  },
11
11
  "./schema": {
12
- "types": "./dist/schema.d.ts",
13
- "import": "./dist/schema.js"
12
+ "types": "./dist/schema.d.mts",
13
+ "import": "./dist/schema.mjs"
14
14
  },
15
15
  "./view-state": {
16
- "types": "./dist/view-state.d.ts",
17
- "import": "./dist/view-state.js"
16
+ "types": "./dist/view-state.d.mts",
17
+ "import": "./dist/view-state.mjs"
18
18
  },
19
19
  "./plugin": {
20
- "types": "./dist/plugin.d.ts",
21
- "import": "./dist/plugin.js"
20
+ "types": "./dist/plugin.d.mts",
21
+ "import": "./dist/plugin.mjs"
22
22
  },
23
23
  "./admin": {
24
- "types": "./dist/admin.d.ts",
25
- "import": "./dist/admin.js"
24
+ "types": "./dist/admin.d.mts",
25
+ "import": "./dist/admin.mjs"
26
26
  }
27
27
  },
28
28
  "files": [
@@ -32,19 +32,19 @@
32
32
  "drizzle-orm": "^0.45.1",
33
33
  "zod": "^3.24.1",
34
34
  "server-only": "^0.0.1",
35
- "@murumets-ee/core": "0.1.5",
36
- "@murumets-ee/db": "0.1.4",
37
- "@murumets-ee/logging": "0.1.5"
35
+ "@murumets-ee/core": "0.1.6",
36
+ "@murumets-ee/db": "0.1.5",
37
+ "@murumets-ee/logging": "0.1.6"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.5",
41
- "tsup": "^8.3.5",
41
+ "tsdown": "^0.21.7",
42
42
  "typescript": "^5.7.3",
43
43
  "vitest": "^2.1.8"
44
44
  },
45
45
  "scripts": {
46
- "build": "rm -rf dist && tsup",
47
- "dev": "tsup --watch",
46
+ "build": "tsdown",
47
+ "dev": "tsdown --watch",
48
48
  "test": "vitest",
49
49
  "test:integration": "vitest run --config vitest.integration.config.ts"
50
50
  }
package/dist/admin.d.ts DELETED
@@ -1,100 +0,0 @@
1
- import { AdminRoute } from '@murumets-ee/core';
2
- import { ZodType } from 'zod';
3
-
4
- /**
5
- * Setting configuration types and compile-time type inference.
6
- *
7
- * Design mirrors the entity field system:
8
- * - Config interfaces define what each setting type accepts
9
- * - SettingToTS maps a single config to its TypeScript type
10
- * - InferSettingValue adds null awareness based on `default` presence
11
- * - InferSettingsMap maps an entire schema to a typed record
12
- */
13
-
14
- interface BaseSettingConfig {
15
- /** Human-readable label for admin UI */
16
- label?: string;
17
- /** Description / help text */
18
- description?: string;
19
- /** If true, this setting can have per-locale values (mirrors entity translatable pattern) */
20
- translatable?: boolean;
21
- }
22
- interface TextSettingConfig extends BaseSettingConfig {
23
- type: 'text';
24
- default?: string;
25
- maxLength?: number;
26
- minLength?: number;
27
- pattern?: RegExp;
28
- }
29
- interface NumberSettingConfig extends BaseSettingConfig {
30
- type: 'number';
31
- default?: number;
32
- min?: number;
33
- max?: number;
34
- integer?: boolean;
35
- }
36
- interface BooleanSettingConfig extends BaseSettingConfig {
37
- type: 'boolean';
38
- default?: boolean;
39
- }
40
- interface SelectSettingConfig<O extends readonly string[] = readonly string[]> extends BaseSettingConfig {
41
- type: 'select';
42
- options: O;
43
- default?: O[number];
44
- }
45
- interface JsonSettingConfig<T = unknown> extends BaseSettingConfig {
46
- type: 'json';
47
- default?: T;
48
- /** Optional Zod schema for validation. If provided, values are validated on set. */
49
- schema?: ZodType<T>;
50
- }
51
- interface MediaSettingConfig extends BaseSettingConfig {
52
- type: 'media';
53
- accept?: string[];
54
- }
55
- type SettingConfig = TextSettingConfig | NumberSettingConfig | BooleanSettingConfig | SelectSettingConfig | JsonSettingConfig | MediaSettingConfig;
56
- type SettingScope = 'global' | 'team' | 'user';
57
- interface SettingsDefinition<S extends Record<string, SettingConfig> = Record<string, SettingConfig>> {
58
- /** Unique namespace for this settings group */
59
- namespace: string;
60
- /** Default scope for these settings */
61
- scope: SettingScope;
62
- /** Setting schema (the shape) */
63
- schema: S;
64
- /** Human-readable label for admin UI */
65
- label?: string;
66
- }
67
-
68
- /**
69
- * Settings admin routes for the centralized admin API handler.
70
- *
71
- * Provides get/update operations for typed settings.
72
- * All auth, CSRF, and role checks are handled by the parent handler.
73
- * Settings mutations require admin role (not editor).
74
- *
75
- * @example
76
- * ```typescript
77
- * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'
78
- * import { settingsRoutes } from '@murumets-ee/settings/admin'
79
- * import { siteSettings } from '@/settings/site'
80
- *
81
- * const handler = createAdminApiHandler({
82
- * authenticate: async (req) => { ... },
83
- * entities: [Article],
84
- * routes: [settingsRoutes(siteSettings)],
85
- * })
86
- * ```
87
- */
88
-
89
- /**
90
- * Create admin API routes for settings management.
91
- *
92
- * Routes:
93
- * - `GET /api/admin/settings` — Get all settings (query: `?locale=`)
94
- * - `PATCH /api/admin/settings` — Update settings (JSON body: `{ values, locale? }`)
95
- *
96
- * @param definition - The settings definition (created by defineSettings)
97
- */
98
- declare function settingsRoutes<S extends Record<string, SettingConfig>>(definition: SettingsDefinition<S>): AdminRoute;
99
-
100
- export { settingsRoutes };
package/dist/admin.js DELETED
@@ -1 +0,0 @@
1
- function u(t,e=200){return new Response(JSON.stringify(t),{status:e,headers:{"Content-Type":"application/json"}})}function p(t,e){return u({error:t},e)}function d(t){let e=null;function c(){return e||(e=(async()=>{let{getApp:s}=await import("@murumets-ee/core"),{createSettingsClient:i}=await import("./client-factory-OGWK5MKO.js"),o=s();return i(t,{app:o})})()),e}async function g(s,i){let o=await c(),r=new URL(s.url).searchParams.get("locale")??void 0,n=await o.getAll(r?{locale:r}:void 0);return u(n)}async function m(s,{user:i,audit:o}){let l=await c(),r=await s.json(),{values:n,locale:a}=r;return!n||typeof n!="object"?p('Body must contain "values" object',400):(await l.setMany(n,a?{locale:a}:void 0),o?.({action:"settings.update",entityType:"settings",userId:i.id,userName:i.name,changes:{fields:n},metadata:{namespace:t.namespace,...a?{locale:a}:{}}}),u({success:!0}))}return{prefix:"settings",resource:"settings",actions:["view","update"],handlers:{GET:g,PATCH:m}}}export{d as settingsRoutes};
@@ -1 +0,0 @@
1
- import{jsonb as u,pgTable as a,timestamp as o,unique as i,uuid as l,varchar as t}from"drizzle-orm/pg-core";var d=a("toolkit_settings",{id:l("id").primaryKey().defaultRandom(),namespace:t("namespace",{length:100}).notNull(),scope:t("scope",{length:20}).notNull().default("global"),scopeId:t("scope_id",{length:100}).notNull().default("__global__"),key:t("key",{length:255}).notNull(),locale:t("locale",{length:10}).notNull().default("_default"),value:u("value"),updatedAt:o("updated_at",{withTimezone:!0}).notNull().defaultNow(),updatedBy:l("updated_by")},e=>({uniqueSetting:i().on(e.namespace,e.scope,e.scopeId,e.key,e.locale)})),s=a("toolkit_view_state",{id:l("id").primaryKey().defaultRandom(),userId:l("user_id").notNull(),viewKey:t("view_key",{length:255}).notNull(),state:u("state").notNull(),expiresAt:o("expires_at",{withTimezone:!0}),updatedAt:o("updated_at",{withTimezone:!0}).notNull().defaultNow()},e=>({uniqueUserView:i().on(e.userId,e.viewKey)}));export{d as a,s as b};
@@ -1 +0,0 @@
1
- import{getApp as L}from"@murumets-ee/core";import{and as S,eq as c,or as k}from"drizzle-orm";import{jsonb as y,pgTable as x,timestamp as w,unique as b,uuid as m,varchar as u}from"drizzle-orm/pg-core";var t=x("toolkit_settings",{id:m("id").primaryKey().defaultRandom(),namespace:u("namespace",{length:100}).notNull(),scope:u("scope",{length:20}).notNull().default("global"),scopeId:u("scope_id",{length:100}).notNull().default("__global__"),key:u("key",{length:255}).notNull(),locale:u("locale",{length:10}).notNull().default("_default"),value:y("value"),updatedAt:w("updated_at",{withTimezone:!0}).notNull().defaultNow(),updatedBy:m("updated_by")},i=>({uniqueSetting:b().on(i.namespace,i.scope,i.scopeId,i.key,i.locale)})),O=x("toolkit_view_state",{id:m("id").primaryKey().defaultRandom(),userId:m("user_id").notNull(),viewKey:u("view_key",{length:255}).notNull(),state:y("state").notNull(),expiresAt:w("expires_at",{withTimezone:!0}),updatedAt:w("updated_at",{withTimezone:!0}).notNull().defaultNow()},i=>({uniqueUserView:b().on(i.userId,i.viewKey)}));var C="__global__",p="_default";import{z as f}from"zod";function I(i){switch(i.type){case"text":{let e=f.string();return i.maxLength&&(e=e.max(i.maxLength)),i.minLength&&(e=e.min(i.minLength)),i.pattern&&(e=e.regex(i.pattern)),e}case"number":{let e=f.number();return i.integer&&(e=e.int()),i.min!==void 0&&(e=e.min(i.min)),i.max!==void 0&&(e=e.max(i.max)),e}case"boolean":return f.boolean();case"select":return f.enum(i.options);case"json":return i.schema??f.unknown();case"media":return f.string().uuid();default:return f.unknown()}}function v(i){let e={};for(let[n,o]of Object.entries(i.schema)){let a=I(o);"default"in o&&o.default!==void 0||(a=a.nullable()),e[n]=a}return e}var h=class{definition;db;logger;scope;scopeId;validators;constructor(e,n){if(typeof window<"u")throw new Error("SettingsClient cannot be used in browser code.");if(this.definition=e,this.db=n.db,this.logger=n.logger,this.scope=n.scope??e.scope,this.scopeId=n.scopeId??C,(this.scope==="team"||this.scope==="user")&&!n.scopeId)throw new Error(`scopeId is required for ${this.scope}-scoped settings (namespace: ${e.namespace})`);this.validators=v(e)}async get(e,n){let o=this.definition.schema[e],a=o?.translatable&&n?.locale?n.locale:null;if(this.logger?.debug({namespace:this.definition.namespace,key:e,locale:a},"Getting setting"),a){let s=await this.db.select({locale:t.locale,value:t.value}).from(t).where(S(...this.baseWhere(),c(t.key,e),k(c(t.locale,a),c(t.locale,p)))),r=s.find(d=>d.locale===a),g=s.find(d=>d.locale===p),l=r??g;if(l&&l.value!==void 0&&l.value!==null)return l.value}else{let s=await this.db.select({value:t.value}).from(t).where(this.whereClause(e)).limit(1);if(s.length>0&&s[0].value!==void 0&&s[0].value!==null)return s[0].value}return o&&"default"in o&&o.default!==void 0?o.default:null}async getAll(e){let n=e?.locale??null;this.logger?.debug({namespace:this.definition.namespace,locale:n},"Getting all settings");let o;n?o=await this.db.select({key:t.key,locale:t.locale,value:t.value}).from(t).where(S(...this.baseWhere(),k(c(t.locale,p),c(t.locale,n)))):o=await this.db.select({key:t.key,locale:t.locale,value:t.value}).from(t).where(S(...this.baseWhere(),c(t.locale,p)));let a=new Map;for(let r of o){let g=a.get(r.key)??{};r.locale===p?g.default=r.value:g.locale=r.value,a.set(r.key,g)}let s={};for(let[r,g]of Object.entries(this.definition.schema)){let l=a.get(r),d;g.translatable&&n&&l?.locale!==void 0&&l?.locale!==null?d=l.locale:l?.default!==void 0&&l?.default!==null&&(d=l.default),d!==void 0?s[r]=d:"default"in g&&g.default!==void 0?s[r]=g.default:s[r]=null}return s}async set(e,n,o){let a=this.resolveLocale(e,o);if(this.logger?.info({namespace:this.definition.namespace,key:e,locale:a},"Setting value"),!(e in this.definition.schema))throw new Error(`Unknown setting key '${e}' in namespace '${this.definition.namespace}'`);let s=this.validators[e];s&&s.parse(n),await this.upsert(e,n,a)}async setMany(e,n){this.logger?.info({namespace:this.definition.namespace,keys:Object.keys(e),locale:n?.locale},"Setting multiple values");for(let[o,a]of Object.entries(e)){if(!(o in this.definition.schema))throw new Error(`Unknown setting key '${o}' in namespace '${this.definition.namespace}'`);let s=this.validators[o];s&&a!==void 0&&s.parse(a)}await this.db.transaction(async o=>{for(let[a,s]of Object.entries(e)){if(s===void 0)continue;let r=this.resolveLocale(a,n);await o.insert(t).values({namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId,key:a,locale:r,value:s,updatedAt:new Date}).onConflictDoUpdate({target:[t.namespace,t.scope,t.scopeId,t.key,t.locale],set:{value:s,updatedAt:new Date}})}})}async delete(e,n){let o=this.resolveLocale(e,n);this.logger?.info({namespace:this.definition.namespace,key:e,locale:o},"Deleting setting"),await this.db.delete(t).where(this.whereClause(e,o))}async has(e,n){let o=this.resolveLocale(e,n);return(await this.db.select({key:t.key}).from(t).where(this.whereClause(e,o)).limit(1)).length>0}resolveLocale(e,n){return this.definition.schema[e]?.translatable&&n?.locale?n.locale:p}baseWhere(){return[c(t.namespace,this.definition.namespace),c(t.scope,this.scope),c(t.scopeId,this.scopeId)]}whereClause(e,n=p){return S(...this.baseWhere(),c(t.key,e),c(t.locale,n))}async upsert(e,n,o){await this.db.insert(t).values({namespace:this.definition.namespace,scope:this.scope,scopeId:this.scopeId,key:e,locale:o,value:n,updatedAt:new Date}).onConflictDoUpdate({target:[t.namespace,t.scope,t.scopeId,t.key,t.locale],set:{value:n,updatedAt:new Date}})}};function E(i,e){let n=e?.app??L();return new h(i,{db:n.db.readWrite,logger:n.logger.child({settings:i.namespace}),scope:e?.scope,scopeId:e?.scopeId})}export{E as createSettingsClient};