@happyvertical/smrt-preferences 0.0.0-stage → 0.55.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,990 @@
1
+ import { GlobalInterceptors, ObjectRegistry, RuntimeError, SmrtCollection, SmrtObject, crossPackageRef, field, resolveGetStringFilter, smrt } from "@happyvertical/smrt-core";
2
+ import { assertOperationPermission, checkOperationPermission, getCurrentSessionPermissionContext, registerPermissionDefinitions } from "@happyvertical/smrt-users";
3
+ import { checkOverviewOverride, diffOverview, loadOverview, optionsEqual, parseOverviewDocument, resolveOverview, sanitizeOverview } from "@happyvertical/smrt-svelte/overview/server";
4
+ import { mergeShellSettingsDelta, sanitizeShellSettingsDelta } from "@happyvertical/smrt-svelte/workspace/server";
5
+ import { TenantIsolationError, TenantScoped, getCurrentTenant, isSuperAdminBypass, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
6
+ //#region src/__smrt-register__.ts
7
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-preferences","packageVersion":"0.55.11","objects":{"@happyvertical/smrt-preferences:UiPreferenceRecordCollection":{"name":"uipreferencerecordcollection","className":"UiPreferenceRecordCollection","qualifiedName":"@happyvertical/smrt-preferences:UiPreferenceRecordCollection","collection":"uipreferencerecords","description":"Reads of UiPreferenceRecord. Not decorated: the model has no generated surface, so a decorated collection would only add a second manifest schema for the same table.","filePath":"/home/runner/work/smrt/smrt/packages/preferences/src/collections/UiPreferenceRecordCollection.ts","packageName":"@happyvertical/smrt-preferences","fields":{},"methods":{"findTier":{"name":"findTier","async":true,"parameters":[{"name":"tenantId","type":"string","optional":false},{"name":"kind","type":"string","optional":false},{"name":"surfaceId","type":"string","optional":false},{"name":"scope","type":"PreferenceScope","optional":false},{"name":"userId","type":"string","optional":true}],"returnType":"Promise<UiPreferenceRecord | null>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_ui_preferences","cli":false},"extends":"SmrtCollection","extendsTypeArg":"UiPreferenceRecord","exportName":"UiPreferenceRecordCollection","collectionExportName":"UiPreferenceRecordCollectionCollection","schema":{"tableName":"_smrt_ui_preferences","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_ui_preferences\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"kind\\" TEXT NOT NULL DEFAULT '',\\n \\"surface_id\\" TEXT NOT NULL DEFAULT '',\\n \\"scope_type\\" TEXT NOT NULL DEFAULT 'tenant',\\n \\"user_id\\" UUID,\\n \\"scope_key\\" TEXT NOT NULL,\\n \\"payload_json\\" TEXT NOT NULL,\\n \\"format_version\\" INTEGER,\\n \\"updated_by\\" UUID\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"kind":{"type":"TEXT","notNull":true,"unique":false,"default":""},"surface_id":{"type":"TEXT","notNull":true,"unique":false,"default":""},"scope_type":{"type":"TEXT","notNull":true,"unique":false,"default":"tenant"},"user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":false,"unique":false},"scope_key":{"type":"TEXT","notNull":true,"unique":false},"payload_json":{"type":"TEXT","notNull":true,"unique":false},"format_version":{"type":"INTEGER","notNull":false,"unique":false},"updated_by":{"type":"UUID","referenceKind":"crossPackageRef","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_ui_preferences_tenant_id_kind_idx","columns":["tenant_id","kind","surface_id","scope_type","scope_key"],"unique":true},{"name":"_smrt_ui_preferences_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_ui_preferences_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"_smrt_ui_preferences_user_id_idx","columns":["user_id"]},{"name":"_smrt_ui_preferences_updated_by_idx","columns":["updated_by"]}],"version":"3f9df5fd"},"importPath":"@happyvertical/smrt-preferences"},"@happyvertical/smrt-preferences:UiPreferenceRecord":{"name":"uipreferencerecord","className":"UiPreferenceRecord","qualifiedName":"@happyvertical/smrt-preferences:UiPreferenceRecord","collection":"uipreferencerecords","description":"One stored user-interface preference (#3727): the canonical payload of a registered preference kind (overview, shell-layout, ...) for one surface at one tier of one tenant.","filePath":"/home/runner/work/smrt/smrt/packages/preferences/src/models/UiPreferenceRecord.ts","packageName":"@happyvertical/smrt-preferences","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"kind":{"type":"text","required":true,"default":"","_meta":{"required":true}},"surfaceId":{"type":"text","required":true,"default":"","_meta":{"required":true}},"scopeType":{"type":"text","required":true,"default":"tenant","_meta":{"required":true}},"userId":{"type":"crossPackageRef","required":false,"related":"@happyvertical/smrt-users:User","_meta":{"nullable":true}},"scopeKey":{"type":"text","required":true,"_meta":{"required":true}},"payloadJson":{"type":"text","required":true,"_meta":{"required":true}},"formatVersion":{"type":"integer","required":false,"_meta":{}},"updatedBy":{"type":"crossPackageRef","required":false,"related":"@happyvertical/smrt-users:User","_meta":{"nullable":true}}},"methods":{"loadFromId":{"name":"loadFromId","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true},"loadFromSlug":{"name":"loadFromSlug","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true},"getPayload":{"name":"getPayload","async":false,"parameters":[],"returnType":"unknown","isStatic":false,"isPublic":true},"setPayload":{"name":"setPayload","async":false,"parameters":[{"name":"value","type":"unknown","optional":false}],"returnType":"void","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SaveOptions","optional":true}],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"delete":{"name":"delete","async":true,"parameters":[{"name":"options","type":"DeleteOptions","optional":true}],"returnType":"Promise<void>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_ui_preferences","conflictColumns":["tenant_id","kind","surface_id","scope_type","scope_key"],"api":{"include":[]},"cli":false,"mcp":{"include":[]},"tenantScoped":{"mode":"required"}},"extends":"SmrtObject","exportName":"UiPreferenceRecord","collectionExportName":"UiPreferenceRecordCollection","validationRules":[{"field":"kind","rule":"required","fieldType":"text"},{"field":"surfaceId","rule":"required","fieldType":"text"},{"field":"scopeType","rule":"required","fieldType":"text"},{"field":"scopeKey","rule":"required","fieldType":"text"},{"field":"payloadJson","rule":"required","fieldType":"text"}],"schema":{"tableName":"_smrt_ui_preferences","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_ui_preferences\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"kind\\" TEXT NOT NULL DEFAULT '',\\n \\"surface_id\\" TEXT NOT NULL DEFAULT '',\\n \\"scope_type\\" TEXT NOT NULL DEFAULT 'tenant',\\n \\"user_id\\" UUID,\\n \\"scope_key\\" TEXT NOT NULL,\\n \\"payload_json\\" TEXT NOT NULL,\\n \\"format_version\\" INTEGER,\\n \\"updated_by\\" UUID\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"kind":{"type":"TEXT","notNull":true,"unique":false,"default":""},"surface_id":{"type":"TEXT","notNull":true,"unique":false,"default":""},"scope_type":{"type":"TEXT","notNull":true,"unique":false,"default":"tenant"},"user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":false,"unique":false},"scope_key":{"type":"TEXT","notNull":true,"unique":false},"payload_json":{"type":"TEXT","notNull":true,"unique":false},"format_version":{"type":"INTEGER","notNull":false,"unique":false},"updated_by":{"type":"UUID","referenceKind":"crossPackageRef","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_ui_preferences_tenant_id_kind_idx","columns":["tenant_id","kind","surface_id","scope_type","scope_key"],"unique":true},{"name":"_smrt_ui_preferences_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_ui_preferences_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"_smrt_ui_preferences_user_id_idx","columns":["user_id"]},{"name":"_smrt_ui_preferences_updated_by_idx","columns":["updated_by"]}],"version":"3f9df5fd"},"importPath":"@happyvertical/smrt-preferences"}},"moduleType":"smrt","smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-tenancy","@happyvertical/smrt-users"],"browser":{"status":"server-only","issues":["#3617","#3624","#3626"],"via":["@happyvertical/smrt-profiles","@happyvertical/smrt-tenancy","@happyvertical/smrt-users"]},"stampsConstructors":true}`));
8
+ //#endregion
9
+ //#region src/permissions.ts
10
+ var CUSTOMIZE_OVERVIEW_PERMISSION = "overviews.customize";
11
+ var PERSONALIZE_OVERVIEW_PERMISSION = "overviews.personalize";
12
+ var CUSTOMIZE_SHELL_PERMISSION = "shell.customize";
13
+ var PERSONALIZE_SHELL_PERMISSION = "shell.personalize";
14
+ var PREFERENCE_PERMISSION_DEFINITIONS = [
15
+ {
16
+ slug: CUSTOMIZE_OVERVIEW_PERMISSION,
17
+ category: "preferences",
18
+ name: "Customize Overviews",
19
+ description: "Change the default overview layouts everyone in the tenant sees"
20
+ },
21
+ {
22
+ slug: PERSONALIZE_OVERVIEW_PERMISSION,
23
+ category: "preferences",
24
+ name: "Personalize Overviews",
25
+ description: "Keep a personal overview layout on top of the tenant default"
26
+ },
27
+ {
28
+ slug: CUSTOMIZE_SHELL_PERMISSION,
29
+ category: "preferences",
30
+ name: "Customize Shell Layout",
31
+ description: "Change the default navigation and panel layout everyone in the tenant sees"
32
+ },
33
+ {
34
+ slug: PERSONALIZE_SHELL_PERMISSION,
35
+ category: "preferences",
36
+ name: "Personalize Shell Layout",
37
+ description: "Keep a personal navigation and panel layout on top of the tenant default"
38
+ }
39
+ ];
40
+ var registered = false;
41
+ function ensurePreferencePermissionsRegistered() {
42
+ if (registered) return;
43
+ registered = true;
44
+ registerPermissionDefinitions(PREFERENCE_PERMISSION_DEFINITIONS);
45
+ }
46
+ function splitPermissionSlug(slug) {
47
+ const dot = slug.lastIndexOf(".");
48
+ if (dot <= 0 || dot === slug.length - 1) throw new Error(`Preference permission "${slug}" is not collection.action`);
49
+ return {
50
+ collection: slug.slice(0, dot),
51
+ action: slug.slice(dot + 1)
52
+ };
53
+ }
54
+ //#endregion
55
+ //#region src/kinds.ts
56
+ var PREFERENCE_SCOPES = ["tenant", "user"];
57
+ var PREFERENCE_KIND_PATTERN = /^[a-z][a-z0-9-]{0,47}$/;
58
+ var PREFERENCE_SURFACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;
59
+ var UnknownPreferenceKindError = class extends Error {
60
+ code = "unknown_preference_kind";
61
+ status = 400;
62
+ constructor(kind) {
63
+ super(`Preference kind "${kind}" is not registered`);
64
+ this.name = "UnknownPreferenceKindError";
65
+ }
66
+ };
67
+ var kinds = /* @__PURE__ */ new Map();
68
+ function registerPreferenceKind(definition, options = {}) {
69
+ if (!PREFERENCE_KIND_PATTERN.test(definition.kind)) throw new Error(`Invalid preference kind "${definition.kind}"`);
70
+ if (!Number.isSafeInteger(definition.formatVersion) || definition.formatVersion < 1) throw new Error(`Preference kind "${definition.kind}" needs a positive formatVersion`);
71
+ splitPermissionSlug(definition.permissions.tenant);
72
+ splitPermissionSlug(definition.permissions.user);
73
+ if (typeof definition.validate !== "function") throw new Error(`Preference kind "${definition.kind}" needs validate()`);
74
+ if (kinds.has(definition.kind) && !options.replace) throw new Error(`Preference kind "${definition.kind}" is already registered`);
75
+ const frozen = Object.freeze({
76
+ ...definition,
77
+ permissions: Object.freeze({ ...definition.permissions })
78
+ });
79
+ kinds.set(definition.kind, frozen);
80
+ return () => {
81
+ if (kinds.get(definition.kind) === frozen) kinds.delete(definition.kind);
82
+ };
83
+ }
84
+ function getPreferenceKind(kind) {
85
+ return kinds.get(kind);
86
+ }
87
+ function requirePreferenceKind(kind) {
88
+ const definition = kinds.get(kind);
89
+ if (!definition) throw new UnknownPreferenceKindError(kind);
90
+ return definition;
91
+ }
92
+ function listPreferenceKinds() {
93
+ return [...kinds.keys()];
94
+ }
95
+ //#endregion
96
+ //#region src/kinds/overview.ts
97
+ var OVERVIEW_PREFERENCE_KIND = "overview";
98
+ function withTenantDefaults(definition, document) {
99
+ return {
100
+ ...definition,
101
+ defaults: document.widgets
102
+ };
103
+ }
104
+ function toPreferenceIssue$1(issue) {
105
+ return {
106
+ path: issue.widgetId,
107
+ code: issue.code,
108
+ message: issue.message,
109
+ detail: issue
110
+ };
111
+ }
112
+ function toOverviewIssue(issue) {
113
+ if (issue.detail && typeof issue.detail === "object") return issue.detail;
114
+ return {
115
+ widgetId: null,
116
+ type: null,
117
+ code: issue.code === "future_version" ? "future_version" : "malformed",
118
+ message: issue.message
119
+ };
120
+ }
121
+ function readOptions(options) {
122
+ if (!options || typeof options !== "object") return null;
123
+ const { definition, registry } = options;
124
+ if (!definition || !registry) return null;
125
+ return {
126
+ definition,
127
+ registry
128
+ };
129
+ }
130
+ function overviewTierDefinition({ definition, registry }, scope, tenantOverride) {
131
+ if (scope === "tenant") return definition;
132
+ return withTenantDefaults(definition, resolveOverview(definition, tenantOverride ?? null, registry).document);
133
+ }
134
+ var overviewPreferenceKind = {
135
+ kind: OVERVIEW_PREFERENCE_KIND,
136
+ formatVersion: 1,
137
+ permissions: {
138
+ tenant: CUSTOMIZE_OVERVIEW_PERMISSION,
139
+ user: PERSONALIZE_OVERVIEW_PERMISSION
140
+ },
141
+ validate(payload, ctx) {
142
+ const options = readOptions(ctx.options);
143
+ if (!options || options.definition.id !== ctx.surfaceId) return {
144
+ ok: false,
145
+ canonical: null,
146
+ issues: [{
147
+ path: null,
148
+ code: "missing_context",
149
+ message: "the overview kind needs { definition, registry } for this surface"
150
+ }]
151
+ };
152
+ const definition = overviewTierDefinition(options, ctx.scope, ctx.tenant);
153
+ if (ctx.phase === "save") {
154
+ const checked = checkOverviewOverride(definition, payload, options.registry);
155
+ return checked.ok ? {
156
+ ok: true,
157
+ canonical: checked.override,
158
+ issues: []
159
+ } : {
160
+ ok: false,
161
+ canonical: null,
162
+ issues: checked.issues.map(toPreferenceIssue$1)
163
+ };
164
+ }
165
+ const resolved = resolveOverview(definition, payload, options.registry);
166
+ return {
167
+ ok: resolved.overrideIssues.length === 0,
168
+ canonical: diffOverview(resolved.base, resolved.document),
169
+ issues: resolved.overrideIssues.map(toPreferenceIssue$1)
170
+ };
171
+ }
172
+ };
173
+ //#endregion
174
+ //#region src/kinds/shell-layout.ts
175
+ var SHELL_LAYOUT_PREFERENCE_KIND = "shell-layout";
176
+ function toPreferenceIssue(issue) {
177
+ return {
178
+ path: issue.path || null,
179
+ code: "invalid_setting",
180
+ message: issue.message,
181
+ detail: issue
182
+ };
183
+ }
184
+ var shellLayoutPreferenceKind = {
185
+ kind: SHELL_LAYOUT_PREFERENCE_KIND,
186
+ formatVersion: 1,
187
+ permissions: {
188
+ tenant: CUSTOMIZE_SHELL_PERMISSION,
189
+ user: PERSONALIZE_SHELL_PERMISSION
190
+ },
191
+ validate(payload) {
192
+ const { delta, issues } = sanitizeShellSettingsDelta(payload);
193
+ return {
194
+ ok: issues.length === 0,
195
+ canonical: Object.keys(delta).length > 0 ? delta : null,
196
+ issues: issues.map(toPreferenceIssue)
197
+ };
198
+ }
199
+ };
200
+ function compact(delta) {
201
+ return Object.fromEntries(Object.entries(delta).filter(([, value]) => value !== void 0));
202
+ }
203
+ function createShellSettingsPreferences(store) {
204
+ return {
205
+ async read(surfaceId) {
206
+ const state = await store.load(SHELL_LAYOUT_PREFERENCE_KIND, surfaceId);
207
+ const tenant = state.tenant.payload ?? null;
208
+ const user = state.user?.payload ?? null;
209
+ return {
210
+ delta: compact(mergeShellSettingsDelta(tenant ?? {}, user ?? {})),
211
+ tenant,
212
+ user,
213
+ revisions: {
214
+ tenant: state.tenant.revision,
215
+ user: state.user?.revision ?? null
216
+ },
217
+ issues: state.issues,
218
+ canCustomize: state.canCustomize
219
+ };
220
+ },
221
+ async write(surfaceId, delta, options = {}) {
222
+ const scope = options.scope ?? "user";
223
+ let revision = options.revision;
224
+ if (revision === void 0) {
225
+ const state = await store.load(SHELL_LAYOUT_PREFERENCE_KIND, surfaceId);
226
+ revision = scope === "tenant" ? state.tenant.revision : state.user?.revision ?? null;
227
+ }
228
+ return store.save(SHELL_LAYOUT_PREFERENCE_KIND, surfaceId, {
229
+ scope,
230
+ payload: delta,
231
+ revision
232
+ });
233
+ },
234
+ reset(surfaceId, options = {}) {
235
+ return store.reset(SHELL_LAYOUT_PREFERENCE_KIND, surfaceId, {
236
+ scope: options.scope ?? "user",
237
+ revision: options.revision
238
+ });
239
+ }
240
+ };
241
+ }
242
+ //#endregion
243
+ //#region src/kinds/builtin.ts
244
+ function ensureBuiltinPreferenceKinds() {
245
+ for (const kind of [overviewPreferenceKind, shellLayoutPreferenceKind]) if (!getPreferenceKind(kind.kind)) registerPreferenceKind(kind);
246
+ }
247
+ ensureBuiltinPreferenceKinds();
248
+ //#endregion
249
+ //#region src/context.ts
250
+ function getPreferencePrincipal() {
251
+ const tenant = getCurrentTenant();
252
+ if (tenant?.tenantId) return {
253
+ tenantId: tenant.tenantId,
254
+ userId: tenant.userId,
255
+ permissions: tenant.permissions,
256
+ superAdminBypass: isSuperAdminBypass(),
257
+ fromTenancy: true
258
+ };
259
+ const session = getCurrentSessionPermissionContext();
260
+ if (!session?.tenantId) return void 0;
261
+ return {
262
+ tenantId: session.tenantId,
263
+ userId: session.userId ?? void 0,
264
+ permissions: session.permissionSet,
265
+ superAdminBypass: session.superAdminBypass,
266
+ fromTenancy: false
267
+ };
268
+ }
269
+ var PreferenceAccessError = class extends TenantIsolationError {
270
+ status = 403;
271
+ publicMessage;
272
+ constructor(message) {
273
+ super(message);
274
+ this.name = "PreferenceAccessError";
275
+ this.publicMessage = message;
276
+ }
277
+ };
278
+ function requirePreferencePrincipal() {
279
+ const principal = getPreferencePrincipal();
280
+ if (!principal) throw new PreferenceAccessError("User-interface preferences need an authenticated tenant context");
281
+ return principal;
282
+ }
283
+ async function withPrincipal(principal, fn) {
284
+ if (principal.fromTenancy) return fn();
285
+ return withTenant({
286
+ tenantId: principal.tenantId,
287
+ userId: principal.userId,
288
+ permissions: new Set(principal.permissions)
289
+ }, fn);
290
+ }
291
+ //#endregion
292
+ //#region src/read-guard.ts
293
+ var GUARD_NAME = "smrt-preferences:owner-reads";
294
+ var CLASS_NAME = "UiPreferenceRecord";
295
+ var QUALIFIED_NAME = "@happyvertical/smrt-preferences:UiPreferenceRecord";
296
+ function field$1(row, camel, snake) {
297
+ if (!row || typeof row !== "object") return void 0;
298
+ const record = row;
299
+ return record[camel] !== void 0 ? record[camel] : record[snake];
300
+ }
301
+ function isPreferenceRowVisible(row) {
302
+ const scope = field$1(row, "scopeType", "scope_type");
303
+ if (scope === "tenant") return true;
304
+ if (scope !== "user") return false;
305
+ const owner = field$1(row, "userId", "user_id");
306
+ const userId = getPreferencePrincipal()?.userId;
307
+ return Boolean(userId) && typeof owner === "string" && owner === userId;
308
+ }
309
+ function isPreferenceClass(className, qualifiedClassName) {
310
+ return qualifiedClassName === QUALIFIED_NAME || className === CLASS_NAME;
311
+ }
312
+ var TENANT_KEY = "__tenant__";
313
+ var NO_ROW_KEY = "";
314
+ var SCOPE_KEY_IN = ["scopeKey in", "scope_key in"];
315
+ function visibleScopeKeys() {
316
+ const userId = getPreferencePrincipal()?.userId;
317
+ return userId ? [TENANT_KEY, userId] : [TENANT_KEY];
318
+ }
319
+ function withOwnerPredicate(where) {
320
+ const visible = visibleScopeKeys();
321
+ const next = { ...where ?? {} };
322
+ let keys = visible;
323
+ for (const key of SCOPE_KEY_IN) {
324
+ if (!(key in next)) continue;
325
+ const requested = next[key];
326
+ delete next[key];
327
+ const list = Array.isArray(requested) ? requested : [requested];
328
+ keys = keys.filter((value) => list.includes(value));
329
+ }
330
+ next["scopeKey in"] = keys.length > 0 ? keys : [NO_ROW_KEY];
331
+ return next;
332
+ }
333
+ function filterRows(className, rows, qualifiedClassName) {
334
+ if (!isPreferenceClass(className, qualifiedClassName)) return void 0;
335
+ return rows.filter(isPreferenceRowVisible);
336
+ }
337
+ function ensurePreferenceReadGuard() {
338
+ if (GlobalInterceptors.getAll().some((entry) => entry.name === GUARD_NAME)) return;
339
+ GlobalInterceptors.register({
340
+ name: GUARD_NAME,
341
+ priority: 90,
342
+ beforeList(className, options, context) {
343
+ if (!isPreferenceClass(className, context.qualifiedClassName)) return;
344
+ const where = options.where;
345
+ if (Array.isArray(where)) return {
346
+ ...options,
347
+ where: where.map((group) => [...group, withOwnerPredicate({})])
348
+ };
349
+ return {
350
+ ...options,
351
+ where: withOwnerPredicate(where)
352
+ };
353
+ },
354
+ beforeGet(className, filter, context) {
355
+ if (!isPreferenceClass(className, context.qualifiedClassName)) return;
356
+ return withOwnerPredicate(typeof filter === "string" ? resolveGetStringFilter(filter) : filter);
357
+ },
358
+ beforeQuery(className, _options, context) {
359
+ if (!isPreferenceClass(className, context.qualifiedClassName)) return;
360
+ throw new PreferenceAccessError("Raw queries over user-interface preferences are not allowed");
361
+ },
362
+ afterList(className, results, context) {
363
+ return filterRows(className, results, context.qualifiedClassName);
364
+ },
365
+ afterQuery(className, results, context) {
366
+ return filterRows(className, results, context.qualifiedClassName);
367
+ },
368
+ afterGet(className, instance, context) {
369
+ if (!instance || !isPreferenceClass(className, context.qualifiedClassName)) return;
370
+ return isPreferenceRowVisible(instance) ? instance : null;
371
+ }
372
+ });
373
+ }
374
+ //#endregion
375
+ //#region src/write-capability.ts
376
+ var STORE_WRITE = /* @__PURE__ */ Symbol("smrt-preferences.storeWrite");
377
+ var TOKEN = Object.freeze({ capability: "smrt-preferences.storeWrite" });
378
+ var writes = /* @__PURE__ */ new WeakMap();
379
+ var deletes = /* @__PURE__ */ new WeakSet();
380
+ function storeWriteOption() {
381
+ return { [STORE_WRITE]: TOKEN };
382
+ }
383
+ function adoptStoreWrite(instance, options) {
384
+ if (options && typeof options === "object" && options[STORE_WRITE] === TOKEN && typeof options.payloadJson === "string") writes.set(instance, options.payloadJson);
385
+ }
386
+ function grantStoreWrite(instance) {
387
+ writes.set(instance, instance.payloadJson);
388
+ }
389
+ function grantStoreDelete(instance) {
390
+ deletes.add(instance);
391
+ }
392
+ function hasStoreWrite(instance, payloadJson) {
393
+ return writes.get(instance) === payloadJson;
394
+ }
395
+ function spendStoreWrite(instance) {
396
+ writes.delete(instance);
397
+ }
398
+ function takeStoreDelete(instance) {
399
+ return deletes.delete(instance);
400
+ }
401
+ //#endregion
402
+ //#region src/models/UiPreferenceRecord.ts
403
+ var __defProp = Object.defineProperty;
404
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
405
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
406
+ enumerable: true,
407
+ configurable: true,
408
+ writable: true,
409
+ value
410
+ }) : obj[key] = value;
411
+ var __decorateClass = (decorators, target, key, kind) => {
412
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
413
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
414
+ if (kind && result) __defProp(target, key, result);
415
+ return result;
416
+ };
417
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
418
+ ensurePreferencePermissionsRegistered();
419
+ ensurePreferenceReadGuard();
420
+ var TENANT_SCOPE_KEY = "__tenant__";
421
+ var UiPreferenceRecord = class extends SmrtObject {
422
+ tenantId = "";
423
+ kind = "";
424
+ surfaceId = "";
425
+ scopeType = "tenant";
426
+ userId = null;
427
+ scopeKey = "";
428
+ payloadJson = "";
429
+ formatVersion = 1;
430
+ updatedBy = null;
431
+ constructor(options = {}) {
432
+ super(options);
433
+ adoptStoreWrite(this, options);
434
+ }
435
+ /**
436
+ * Single-row hydration (`new UiPreferenceRecord({ id }).initialize()`)
437
+ * reads the table directly, outside the collection interceptors, so the
438
+ * owner check runs here: another user's row is wiped and refused.
439
+ */
440
+ async loadFromId() {
441
+ await super.loadFromId();
442
+ this.assertReadable();
443
+ }
444
+ async loadFromSlug() {
445
+ await super.loadFromSlug();
446
+ this.assertReadable();
447
+ }
448
+ assertReadable() {
449
+ if (!this.kind || isPreferenceRowVisible(this)) return;
450
+ this.payloadJson = "";
451
+ throw new PreferenceAccessError("A personal preference can only be read by its owner");
452
+ }
453
+ /** The stored payload, parsed. `undefined` when the text is not JSON. */
454
+ getPayload() {
455
+ try {
456
+ return JSON.parse(this.payloadJson);
457
+ } catch {
458
+ return;
459
+ }
460
+ }
461
+ /** Store a payload; the caller passes the kind's canonical value. */
462
+ setPayload(value) {
463
+ this.payloadJson = JSON.stringify(value);
464
+ }
465
+ /** The optimistic-concurrency token a client echoes back on save. */
466
+ get revision() {
467
+ const at = this.updated_at;
468
+ if (!at) return null;
469
+ const date = at instanceof Date ? at : new Date(at);
470
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
471
+ }
472
+ async save(options = {}) {
473
+ const principal = requirePreferencePrincipal();
474
+ if (!this.tenantId) this.tenantId = principal.tenantId;
475
+ this.syncScopeKey();
476
+ await this.assertPersistedIdentityUnchanged();
477
+ this.assertStoreWrite();
478
+ this.validateShape();
479
+ await this.authorize();
480
+ this.updatedBy = principal.userId ?? null;
481
+ try {
482
+ return await super.save(options);
483
+ } finally {
484
+ spendStoreWrite(this);
485
+ }
486
+ }
487
+ /**
488
+ * Runs for ordinary saves and bulk creates alike, so a collection-level
489
+ * bulk insert is refused too (`save()` checks first so callers see it).
490
+ */
491
+ async validateBeforeSave() {
492
+ await super.validateBeforeSave();
493
+ this.assertStoreWrite();
494
+ }
495
+ async delete(options = {}) {
496
+ if (!takeStoreDelete(this)) throw new PreferenceAccessError("Preferences are reset only through the preference store");
497
+ this.syncScopeKey();
498
+ await this.assertPersistedIdentityUnchanged();
499
+ await this.authorize();
500
+ return super.delete(options);
501
+ }
502
+ /**
503
+ * Only the store, after the kind's validator accepted exactly this
504
+ * payload, may write a row (see `write-capability.ts`).
505
+ */
506
+ assertStoreWrite() {
507
+ if (!hasStoreWrite(this, this.payloadJson)) throw new PreferenceAccessError("Preferences are written only through the preference store, which validates them");
508
+ }
509
+ syncScopeKey() {
510
+ if (this.scopeType === "tenant" && this.userId === null) this.scopeKey = TENANT_SCOPE_KEY;
511
+ else if (this.scopeType === "user" && this.userId) this.scopeKey = this.userId;
512
+ }
513
+ validateShape() {
514
+ if (!PREFERENCE_KIND_PATTERN.test(this.kind)) throw new Error("UiPreferenceRecord.kind is not a valid kind id");
515
+ if (!PREFERENCE_SURFACE_PATTERN.test(this.surfaceId)) throw new Error("UiPreferenceRecord.surfaceId is not a valid id");
516
+ if (!PREFERENCE_SCOPES.includes(this.scopeType)) throw new Error("UiPreferenceRecord.scopeType must be tenant or user");
517
+ if (this.scopeType === "tenant" && this.userId !== null) throw new Error("A tenant-default preference row has no userId");
518
+ if (this.scopeType === "user" && !this.userId) throw new Error("A user preference row needs its userId");
519
+ if (!Number.isSafeInteger(this.formatVersion) || this.formatVersion < 1) throw new Error("UiPreferenceRecord.formatVersion must be positive");
520
+ if (this.getPayload() === void 0) throw new Error("UiPreferenceRecord.payloadJson must be JSON");
521
+ }
522
+ /**
523
+ * Fail-closed tier guard. The tenant must be the principal's (the
524
+ * interceptor checks this too; a super-admin bypass does not filter reads,
525
+ * so it is re-checked here), a user row must belong to the principal (a
526
+ * principal with no user id may not touch the user tier), the kind must be
527
+ * registered, and its permission for the tier must be held.
528
+ */
529
+ async authorize() {
530
+ const principal = requirePreferencePrincipal();
531
+ const kind = requirePreferenceKind(this.kind);
532
+ if (this.tenantId !== principal.tenantId) throw new PreferenceAccessError("This preference belongs to another tenant");
533
+ if (this.scopeType === "user") {
534
+ if (!principal.userId || this.userId !== principal.userId) throw new PreferenceAccessError("A personal preference can only be changed by its owner");
535
+ }
536
+ await assertOperationPermission({
537
+ ...splitPermissionSlug(this.scopeType === "user" ? kind.permissions.user : kind.permissions.tenant),
538
+ db: this.options.db ?? this.options.persistence,
539
+ tenantId: principal.tenantId,
540
+ userId: principal.userId ?? null,
541
+ permissionSet: principal.permissions
542
+ });
543
+ }
544
+ /**
545
+ * A persisted row keeps its identity: the stored tenant, kind, surface and
546
+ * tier must be the ones in memory, so {@link authorize} always judges the
547
+ * row as stored. Matched by predicate rather than by reading the columns
548
+ * back, because DuckDB returns raw UUID columns as lossy HUGEINTs.
549
+ */
550
+ async assertPersistedIdentityUnchanged() {
551
+ if (!this.id) return;
552
+ if (!await this.db.get(this.tableName, { id: this.id })) return;
553
+ if (!await this.db.get(this.tableName, {
554
+ id: this.id,
555
+ tenant_id: this.tenantId,
556
+ kind: this.kind,
557
+ surface_id: this.surfaceId,
558
+ scope_type: this.scopeType,
559
+ scope_key: this.scopeKey
560
+ })) throw new PreferenceAccessError("A preference row cannot change its tenant, kind, surface or owner");
561
+ }
562
+ };
563
+ __publicField(UiPreferenceRecord, "__smrtPackage__", "@happyvertical/smrt-preferences");
564
+ __decorateClass([tenantId()], UiPreferenceRecord.prototype, "tenantId", 2);
565
+ __decorateClass([field({ required: true })], UiPreferenceRecord.prototype, "kind", 2);
566
+ __decorateClass([field({ required: true })], UiPreferenceRecord.prototype, "surfaceId", 2);
567
+ __decorateClass([field({ required: true })], UiPreferenceRecord.prototype, "scopeType", 2);
568
+ __decorateClass([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], UiPreferenceRecord.prototype, "userId", 2);
569
+ __decorateClass([field({
570
+ type: "text",
571
+ required: true
572
+ })], UiPreferenceRecord.prototype, "scopeKey", 2);
573
+ __decorateClass([field({
574
+ type: "text",
575
+ required: true
576
+ })], UiPreferenceRecord.prototype, "payloadJson", 2);
577
+ __decorateClass([field({ type: "integer" })], UiPreferenceRecord.prototype, "formatVersion", 2);
578
+ __decorateClass([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], UiPreferenceRecord.prototype, "updatedBy", 2);
579
+ UiPreferenceRecord = __decorateClass([TenantScoped({ mode: "required" }), smrt({
580
+ tableName: "_smrt_ui_preferences",
581
+ conflictColumns: [
582
+ "tenant_id",
583
+ "kind",
584
+ "surface_id",
585
+ "scope_type",
586
+ "scope_key"
587
+ ],
588
+ api: { include: [] },
589
+ cli: false,
590
+ mcp: { include: [] }
591
+ })], UiPreferenceRecord);
592
+ //#endregion
593
+ //#region src/collections/UiPreferenceRecordCollection.ts
594
+ var UiPreferenceRecordCollection = class extends SmrtCollection {
595
+ static _itemClass = UiPreferenceRecord;
596
+ /**
597
+ * The row of one tier, or `null`. The tenant predicate is explicit (not
598
+ * only the interceptor's) because a super-admin bypass context does not
599
+ * filter reads.
600
+ */
601
+ async findTier(tenantId, kind, surfaceId, scope, userId) {
602
+ const scopeKey = scope === "tenant" ? TENANT_SCOPE_KEY : userId;
603
+ if (!scopeKey) return null;
604
+ return (await this.list({
605
+ where: {
606
+ tenantId,
607
+ kind,
608
+ surfaceId,
609
+ scopeType: scope,
610
+ scopeKey
611
+ },
612
+ limit: 1
613
+ }))[0] ?? null;
614
+ }
615
+ };
616
+ //#endregion
617
+ //#region src/store.ts
618
+ function isRevisionConflict(error) {
619
+ return error instanceof RuntimeError && error.code === "RUNTIME_REVISION_CONFLICT";
620
+ }
621
+ function assertSurfaceId(surfaceId) {
622
+ if (!PREFERENCE_SURFACE_PATTERN.test(surfaceId)) throw new Error(`Invalid preference surface id "${surfaceId}"`);
623
+ }
624
+ async function holds(principal, slug, db) {
625
+ return (await checkOperationPermission({
626
+ ...splitPermissionSlug(slug),
627
+ db,
628
+ tenantId: principal.tenantId,
629
+ userId: principal.userId ?? null,
630
+ permissionSet: principal.permissions,
631
+ onDeny: "return"
632
+ })).allowed;
633
+ }
634
+ async function canWrite(principal, kind, scope, db) {
635
+ if (scope === "user") return Boolean(principal.userId) && await holds(principal, kind.permissions.user, db);
636
+ return holds(principal, kind.permissions.tenant, db);
637
+ }
638
+ function readTier(kind, surfaceId, scope, row, options, tenant) {
639
+ if (!row) return {
640
+ payload: null,
641
+ revision: null,
642
+ issues: []
643
+ };
644
+ const revision = row.revision;
645
+ const payload = row.getPayload();
646
+ if (payload === void 0) return {
647
+ payload: null,
648
+ revision,
649
+ issues: [{
650
+ path: null,
651
+ code: "malformed",
652
+ message: "stored payload is not JSON"
653
+ }]
654
+ };
655
+ if (row.formatVersion > kind.formatVersion) return {
656
+ payload: null,
657
+ revision,
658
+ issues: [{
659
+ path: null,
660
+ code: "future_version",
661
+ message: "stored payload was written by a newer format version"
662
+ }]
663
+ };
664
+ let checked;
665
+ try {
666
+ checked = kind.validate(payload, {
667
+ kind: kind.kind,
668
+ surfaceId,
669
+ scope,
670
+ phase: "load",
671
+ formatVersion: row.formatVersion,
672
+ ...scope === "user" ? { tenant: tenant ?? null } : {},
673
+ options
674
+ });
675
+ } catch {
676
+ return {
677
+ payload: null,
678
+ revision,
679
+ issues: [{
680
+ path: null,
681
+ code: "validator_error",
682
+ message: "the preference kind failed to validate the stored payload"
683
+ }]
684
+ };
685
+ }
686
+ return {
687
+ payload: checked.canonical,
688
+ revision,
689
+ issues: checked.issues
690
+ };
691
+ }
692
+ function createPreferenceStore(options) {
693
+ const { db } = options;
694
+ const collection = () => {
695
+ ensurePreferenceReadGuard();
696
+ return UiPreferenceRecordCollection.create({ db });
697
+ };
698
+ async function tenantTier(records, principal, kind, surfaceId, kindOptions) {
699
+ return readTier(kind, surfaceId, "tenant", await records.findTier(principal.tenantId, kind.kind, surfaceId, "tenant"), kindOptions);
700
+ }
701
+ return {
702
+ async load(kindId, surfaceId, input = {}) {
703
+ const kind = requirePreferenceKind(kindId);
704
+ assertSurfaceId(surfaceId);
705
+ const principal = requirePreferencePrincipal();
706
+ return withPrincipal(principal, async () => {
707
+ const records = await collection();
708
+ const tenant = await tenantTier(records, principal, kind, surfaceId, input.options);
709
+ const userRow = principal.userId ? await records.findTier(principal.tenantId, kind.kind, surfaceId, "user", principal.userId) : null;
710
+ const user = principal.userId ? readTier(kind, surfaceId, "user", userRow, input.options, tenant.payload) : null;
711
+ const [tenantWrite, userWrite] = await Promise.all([canWrite(principal, kind, "tenant", db), canWrite(principal, kind, "user", db)]);
712
+ return {
713
+ kind: kind.kind,
714
+ surfaceId,
715
+ tenant,
716
+ user,
717
+ issues: [...tenant.issues, ...user?.issues ?? []],
718
+ canCustomize: {
719
+ tenant: tenantWrite,
720
+ user: userWrite
721
+ }
722
+ };
723
+ });
724
+ },
725
+ async save(kindId, surfaceId, input) {
726
+ const kind = requirePreferenceKind(kindId);
727
+ assertSurfaceId(surfaceId);
728
+ const principal = requirePreferencePrincipal();
729
+ return withPrincipal(principal, async () => {
730
+ if (!await canWrite(principal, kind, input.scope, db)) return {
731
+ ok: false,
732
+ reason: "not_allowed"
733
+ };
734
+ const records = await collection();
735
+ const tenant = input.scope === "user" ? (await tenantTier(records, principal, kind, surfaceId, input.options)).payload : void 0;
736
+ const checked = kind.validate(input.payload, {
737
+ kind: kind.kind,
738
+ surfaceId,
739
+ scope: input.scope,
740
+ phase: "save",
741
+ formatVersion: kind.formatVersion,
742
+ ...input.scope === "user" ? { tenant: tenant ?? null } : {},
743
+ options: input.options
744
+ });
745
+ if (!checked.ok) return {
746
+ ok: false,
747
+ reason: "invalid",
748
+ issues: checked.issues
749
+ };
750
+ const canonical = checked.canonical ?? null;
751
+ const userId = input.scope === "user" ? principal.userId : void 0;
752
+ const existing = await records.findTier(principal.tenantId, kind.kind, surfaceId, input.scope, userId);
753
+ const revision = input.revision ?? null;
754
+ if (canonical === null) {
755
+ if (!existing) return revision === null ? {
756
+ ok: true,
757
+ payload: null,
758
+ revision: null
759
+ } : {
760
+ ok: false,
761
+ reason: "conflict"
762
+ };
763
+ if (revision === null) return {
764
+ ok: false,
765
+ reason: "conflict"
766
+ };
767
+ try {
768
+ grantStoreDelete(existing);
769
+ await existing.delete({ expectedUpdatedAt: revision });
770
+ } catch (error) {
771
+ if (isRevisionConflict(error)) return {
772
+ ok: false,
773
+ reason: "conflict"
774
+ };
775
+ throw error;
776
+ }
777
+ return {
778
+ ok: true,
779
+ payload: null,
780
+ revision: null
781
+ };
782
+ }
783
+ if (existing) {
784
+ if (revision === null) return {
785
+ ok: false,
786
+ reason: "conflict"
787
+ };
788
+ existing.setPayload(canonical);
789
+ existing.formatVersion = kind.formatVersion;
790
+ grantStoreWrite(existing);
791
+ try {
792
+ await existing.save({ expectedUpdatedAt: revision });
793
+ } catch (error) {
794
+ if (isRevisionConflict(error)) return {
795
+ ok: false,
796
+ reason: "conflict"
797
+ };
798
+ throw error;
799
+ }
800
+ return {
801
+ ok: true,
802
+ payload: canonical,
803
+ revision: existing.revision
804
+ };
805
+ }
806
+ if (revision !== null) return {
807
+ ok: false,
808
+ reason: "conflict"
809
+ };
810
+ try {
811
+ return {
812
+ ok: true,
813
+ payload: canonical,
814
+ revision: (await records.create({
815
+ tenantId: principal.tenantId,
816
+ kind: kind.kind,
817
+ surfaceId,
818
+ scopeType: input.scope,
819
+ userId: userId ?? null,
820
+ payloadJson: JSON.stringify(canonical),
821
+ formatVersion: kind.formatVersion,
822
+ _insertOnly: true,
823
+ ...storeWriteOption()
824
+ })).revision
825
+ };
826
+ } catch (error) {
827
+ if (await records.findTier(principal.tenantId, kind.kind, surfaceId, input.scope, userId)) return {
828
+ ok: false,
829
+ reason: "conflict"
830
+ };
831
+ throw error;
832
+ }
833
+ });
834
+ },
835
+ async reset(kindId, surfaceId, input) {
836
+ const kind = requirePreferenceKind(kindId);
837
+ assertSurfaceId(surfaceId);
838
+ const principal = requirePreferencePrincipal();
839
+ return withPrincipal(principal, async () => {
840
+ if (!await canWrite(principal, kind, input.scope, db)) return {
841
+ ok: false,
842
+ reason: "not_allowed"
843
+ };
844
+ const existing = await (await collection()).findTier(principal.tenantId, kind.kind, surfaceId, input.scope, input.scope === "user" ? principal.userId : void 0);
845
+ if (!existing) return { ok: true };
846
+ if (input.revision === null) return {
847
+ ok: false,
848
+ reason: "conflict"
849
+ };
850
+ try {
851
+ grantStoreDelete(existing);
852
+ await existing.delete(typeof input.revision === "string" ? { expectedUpdatedAt: input.revision } : {});
853
+ } catch (error) {
854
+ if (isRevisionConflict(error)) return {
855
+ ok: false,
856
+ reason: "conflict"
857
+ };
858
+ throw error;
859
+ }
860
+ return { ok: true };
861
+ });
862
+ }
863
+ };
864
+ }
865
+ //#endregion
866
+ //#region src/overview.ts
867
+ function sameWidget(a, b) {
868
+ return a.id === b.id && a.type === b.type && (a.version ?? 1) === (b.version ?? 1) && optionsEqual(a.options, b.options);
869
+ }
870
+ function createOverviewStore(options) {
871
+ const preferences = options.preferences ?? createPreferenceStore({ db: options.db });
872
+ const kindOptions = (definition, registry) => ({
873
+ definition,
874
+ registry
875
+ });
876
+ const store = {
877
+ async load(definition, registry) {
878
+ const state = await preferences.load(OVERVIEW_PREFERENCE_KIND, definition.id, { options: kindOptions(definition, registry) });
879
+ const tenantOverride = state.tenant.payload;
880
+ const tenantResolved = resolveOverview(definition, tenantOverride, registry);
881
+ const tenant = {
882
+ base: tenantResolved.base,
883
+ document: tenantResolved.document,
884
+ override: tenantOverride,
885
+ revision: state.tenant.revision,
886
+ issues: state.tenant.issues.map(toOverviewIssue)
887
+ };
888
+ let user = null;
889
+ if (state.user) {
890
+ const userOverride = state.user.payload;
891
+ const userResolved = resolveOverview(withTenantDefaults(definition, tenant.document), userOverride, registry);
892
+ user = {
893
+ base: userResolved.base,
894
+ document: userResolved.document,
895
+ override: userOverride,
896
+ revision: state.user.revision,
897
+ issues: state.user.issues.map(toOverviewIssue)
898
+ };
899
+ }
900
+ return {
901
+ overviewId: definition.id,
902
+ tenant,
903
+ user,
904
+ document: user?.document ?? tenant.document,
905
+ issues: [...tenant.issues, ...user?.issues ?? []],
906
+ canCustomize: state.canCustomize
907
+ };
908
+ },
909
+ async save(definition, registry, input) {
910
+ const result = await preferences.save(OVERVIEW_PREFERENCE_KIND, definition.id, {
911
+ scope: input.scope,
912
+ payload: input.override,
913
+ revision: input.revision,
914
+ options: kindOptions(definition, registry)
915
+ });
916
+ if (result.ok) return {
917
+ ok: true,
918
+ override: result.payload,
919
+ revision: result.revision
920
+ };
921
+ if (result.reason === "invalid") return {
922
+ ok: false,
923
+ reason: "invalid",
924
+ issues: result.issues.map(toOverviewIssue)
925
+ };
926
+ return result;
927
+ },
928
+ async reset(definition, input) {
929
+ const result = await preferences.reset(OVERVIEW_PREFERENCE_KIND, definition.id, input);
930
+ if (!result.ok && result.reason === "invalid") return {
931
+ ok: false,
932
+ reason: "invalid",
933
+ issues: result.issues.map(toOverviewIssue)
934
+ };
935
+ return result;
936
+ },
937
+ async loadPage(definition, registry, ctx, pageOptions = {}) {
938
+ const { scope = "user", ...loadOptions } = pageOptions;
939
+ const state = await store.load(definition, registry);
940
+ const loaded = await loadOverview(scope === "tenant" ? state.tenant.document : state.document, definition, registry, ctx, loadOptions);
941
+ return {
942
+ ...state,
943
+ loaded
944
+ };
945
+ },
946
+ async loadWidget(definition, registry, widget, ctx, loadOptions = {}) {
947
+ const state = await store.load(definition, registry);
948
+ const parsed = parseOverviewDocument({ widgets: [widget] });
949
+ const candidate = parsed.document.widgets[0];
950
+ if (!candidate || parsed.issues.length > 0) return {
951
+ ok: false,
952
+ reason: "invalid",
953
+ issues: parsed.issues
954
+ };
955
+ if (![...state.document.widgets, ...state.tenant.document.widgets].some((existing) => sameWidget(existing, candidate)) && !state.canCustomize.tenant && !state.canCustomize.user) return {
956
+ ok: false,
957
+ reason: "not_allowed"
958
+ };
959
+ const sanitized = sanitizeOverview({ widgets: [candidate] }, {
960
+ registry,
961
+ definition
962
+ });
963
+ if (sanitized.issues.length > 0) return {
964
+ ok: false,
965
+ reason: "invalid",
966
+ issues: sanitized.issues
967
+ };
968
+ const result = (await loadOverview(sanitized.document, definition, registry, ctx, loadOptions)).widgets[0];
969
+ if (!result) return {
970
+ ok: false,
971
+ reason: "invalid",
972
+ issues: []
973
+ };
974
+ if (result.status === "error") return {
975
+ ok: false,
976
+ reason: "load_failed",
977
+ code: result.error?.code ?? "load_failed"
978
+ };
979
+ return {
980
+ ok: true,
981
+ data: result.data
982
+ };
983
+ }
984
+ };
985
+ return store;
986
+ }
987
+ //#endregion
988
+ export { CUSTOMIZE_OVERVIEW_PERMISSION, CUSTOMIZE_SHELL_PERMISSION, OVERVIEW_PREFERENCE_KIND, PERSONALIZE_OVERVIEW_PERMISSION, PERSONALIZE_SHELL_PERMISSION, PREFERENCE_KIND_PATTERN, PREFERENCE_PERMISSION_DEFINITIONS, PREFERENCE_SCOPES, PREFERENCE_SURFACE_PATTERN, PreferenceAccessError, SHELL_LAYOUT_PREFERENCE_KIND, TENANT_SCOPE_KEY, UiPreferenceRecord, UnknownPreferenceKindError, createOverviewStore, createPreferenceStore, createShellSettingsPreferences, ensureBuiltinPreferenceKinds, ensurePreferencePermissionsRegistered, getPreferenceKind, getPreferencePrincipal, listPreferenceKinds, overviewPreferenceKind, registerPreferenceKind, requirePreferenceKind, shellLayoutPreferenceKind, splitPermissionSlug, withTenantDefaults };
989
+
990
+ //# sourceMappingURL=index.js.map