@happyvertical/smrt-fields 0.40.61 → 0.40.63
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/AGENTS.md +18 -0
- package/README.md +125 -112
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js +1218 -0
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js.map +1 -0
- package/dist/collections/FieldPolicySuggestionCollection.d.ts +160 -0
- package/dist/collections/FieldPolicySuggestionCollection.d.ts.map +1 -0
- package/dist/collections/FieldUsageCounterCollection.d.ts +143 -0
- package/dist/collections/FieldUsageCounterCollection.d.ts.map +1 -0
- package/dist/deterministic-id.d.ts +18 -0
- package/dist/deterministic-id.d.ts.map +1 -0
- package/dist/field-definitions.d.ts +8 -0
- package/dist/field-definitions.d.ts.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1565 -1077
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1442 -188
- package/dist/models/FieldPolicySuggestion.d.ts +132 -0
- package/dist/models/FieldPolicySuggestion.d.ts.map +1 -0
- package/dist/models/FieldUsageCounter.d.ts +178 -0
- package/dist/models/FieldUsageCounter.d.ts.map +1 -0
- package/dist/models/FieldUsageReportReceipt.d.ts +25 -0
- package/dist/models/FieldUsageReportReceipt.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +404 -7
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +54 -0
- package/dist/svelte/__tests__/FormHelp.test.js +8 -0
- package/dist/svelte/__tests__/ObjectForm.test.js +131 -0
- package/dist/svelte/__tests__/UsageLearning.test.js +140 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +21 -4
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +5 -1
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -1
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte +23 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/playground.test.js +66 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte +28 -1
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +3 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -1
- package/dist/svelte/components/FieldPolicyGearButton.svelte +8 -1
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -1
- package/dist/svelte/components/FieldPolicyGearProvider.svelte +34 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +3 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -1
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte +135 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts +11 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FormHelp.svelte +16 -1
- package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ObjectForm.svelte +91 -2
- package/dist/svelte/components/ObjectForm.svelte.d.ts +11 -1
- package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -1
- package/dist/svelte/context.svelte.d.ts +15 -0
- package/dist/svelte/context.svelte.d.ts.map +1 -1
- package/dist/svelte/context.svelte.js +15 -0
- package/dist/svelte/gear-context.svelte.d.ts +2 -0
- package/dist/svelte/gear-context.svelte.d.ts.map +1 -1
- package/dist/svelte/index.d.ts +6 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte +18 -12
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -1
- package/dist/svelte/suggestions.d.ts +38 -0
- package/dist/svelte/suggestions.d.ts.map +1 -0
- package/dist/svelte/suggestions.js +68 -0
- package/dist/svelte/usage-capture.d.ts +63 -0
- package/dist/svelte/usage-capture.d.ts.map +1 -0
- package/dist/svelte/usage-capture.js +61 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/dist/usage-learning.d.ts +201 -0
- package/dist/usage-learning.d.ts.map +1 -0
- package/dist/usage-schedules.d.ts +110 -0
- package/dist/usage-schedules.d.ts.map +1 -0
- package/dist/users-module.d.ts +30 -0
- package/dist/users-module.d.ts.map +1 -0
- package/package.json +9 -9
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import { FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES } from "../types.js";
|
|
2
|
+
import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
|
|
3
|
+
import { MembershipCollection, assertOperationPermission, checkOperationPermission, getCurrentSessionPermissionContext, registerPermissionDefinitions } from "@happyvertical/smrt-users";
|
|
4
|
+
import { TenantIsolationError, getCurrentTenant, isSuperAdminBypass, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __defProp$2 = Object.defineProperty;
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp$2(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp$2(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/permissions.ts
|
|
18
|
+
var MANAGE_FIELD_POLICY_PERMISSION = "fields.policy.manage";
|
|
19
|
+
var PERSONALIZE_FIELD_POLICY_PERMISSION = "fields.policy.personalize";
|
|
20
|
+
var FIELD_POLICY_PERMISSION_DEFINITIONS = [{
|
|
21
|
+
slug: MANAGE_FIELD_POLICY_PERMISSION,
|
|
22
|
+
category: "fields",
|
|
23
|
+
name: "Manage Field Policies",
|
|
24
|
+
description: "Manage application and tenant field-policy overrides"
|
|
25
|
+
}, {
|
|
26
|
+
slug: PERSONALIZE_FIELD_POLICY_PERMISSION,
|
|
27
|
+
category: "fields",
|
|
28
|
+
name: "Personalize Field Policies",
|
|
29
|
+
description: "Manage the caller’s own field-policy overrides"
|
|
30
|
+
}];
|
|
31
|
+
var registered = false;
|
|
32
|
+
function ensureFieldPolicyPermissionsRegistered() {
|
|
33
|
+
if (registered) return;
|
|
34
|
+
registered = true;
|
|
35
|
+
registerPermissionDefinitions(FIELD_POLICY_PERMISSION_DEFINITIONS);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/cache.ts
|
|
39
|
+
var FIELD_POLICY_CACHE_TTL_MS = 3e4;
|
|
40
|
+
var fieldPolicyCache = /* @__PURE__ */ new Map();
|
|
41
|
+
var dbInstanceIds = /* @__PURE__ */ new WeakMap();
|
|
42
|
+
var nextDbId = 1;
|
|
43
|
+
function getDbNamespace(db) {
|
|
44
|
+
if (!db) return "no-db";
|
|
45
|
+
if (typeof db === "string") return `db:${db}`;
|
|
46
|
+
if (typeof db === "object") {
|
|
47
|
+
const dbObject = db;
|
|
48
|
+
if (typeof dbObject.query === "function") {
|
|
49
|
+
if (!dbInstanceIds.has(dbObject)) dbInstanceIds.set(dbObject, `db-instance:${nextDbId++}`);
|
|
50
|
+
const namespace = dbInstanceIds.get(dbObject);
|
|
51
|
+
if (namespace) return namespace;
|
|
52
|
+
return "db-instance:unknown";
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return `db-config:${JSON.stringify(dbObject)}`;
|
|
56
|
+
} catch {
|
|
57
|
+
return "db-config:opaque";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return "db:unknown";
|
|
61
|
+
}
|
|
62
|
+
var hierarchyLoaderIds = /* @__PURE__ */ new WeakMap();
|
|
63
|
+
var nextLoaderId = 1;
|
|
64
|
+
function getHierarchyLoaderNamespace(loader) {
|
|
65
|
+
if (typeof loader !== "function") return "loader:default";
|
|
66
|
+
const key = loader;
|
|
67
|
+
if (!hierarchyLoaderIds.has(key)) hierarchyLoaderIds.set(key, `loader:${nextLoaderId++}`);
|
|
68
|
+
return hierarchyLoaderIds.get(key) ?? "loader:unknown";
|
|
69
|
+
}
|
|
70
|
+
function buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
71
|
+
return `${getDbNamespace(db)}::${objectRef}::${tenantId ?? "app"}::${userId ?? "user:none"}::${getHierarchyLoaderNamespace(hierarchyLoader)}`;
|
|
72
|
+
}
|
|
73
|
+
function getFieldPolicyCacheTtlMs() {
|
|
74
|
+
return FIELD_POLICY_CACHE_TTL_MS;
|
|
75
|
+
}
|
|
76
|
+
function getCachedFieldPolicy(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
77
|
+
const cacheKey = buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader);
|
|
78
|
+
const cached = fieldPolicyCache.get(cacheKey);
|
|
79
|
+
if (!cached) return null;
|
|
80
|
+
if (cached.expiresAt <= Date.now()) {
|
|
81
|
+
fieldPolicyCache.delete(cacheKey);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return cached.value;
|
|
85
|
+
}
|
|
86
|
+
function setCachedFieldPolicy(objectRef, tenantId, userId, db, value, hierarchyLoader) {
|
|
87
|
+
fieldPolicyCache.set(buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader), {
|
|
88
|
+
expiresAt: Date.now() + FIELD_POLICY_CACHE_TTL_MS,
|
|
89
|
+
value
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function invalidateFieldPolicyCache(objectRef, db) {
|
|
93
|
+
const keyPrefix = `${getDbNamespace(db)}::${objectRef}::`;
|
|
94
|
+
for (const cacheKey of fieldPolicyCache.keys()) if (cacheKey.startsWith(keyPrefix)) fieldPolicyCache.delete(cacheKey);
|
|
95
|
+
}
|
|
96
|
+
function clearFieldPolicyCache() {
|
|
97
|
+
fieldPolicyCache.clear();
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/field-definitions.ts
|
|
101
|
+
function requireRegisteredObject(objectRef) {
|
|
102
|
+
if (!objectRef?.includes(":")) throw new Error(`Field policy objectRef must be a qualified class name ("@package/name:ClassName"), got "${objectRef}"`);
|
|
103
|
+
if (!ObjectRegistry.getClassByQualifiedName(objectRef)) throw new Error(`Unknown field policy objectRef "${objectRef}": no class with that qualified name is registered`);
|
|
104
|
+
}
|
|
105
|
+
async function getObjectFieldMap(objectRef) {
|
|
106
|
+
requireRegisteredObject(objectRef);
|
|
107
|
+
return ObjectRegistry.getAllFields(objectRef);
|
|
108
|
+
}
|
|
109
|
+
function sanitizeFieldUIHints(value) {
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
111
|
+
const raw = value;
|
|
112
|
+
const ui = {
|
|
113
|
+
...typeof raw.basic === "boolean" ? { basic: raw.basic } : {},
|
|
114
|
+
...typeof raw.group === "string" ? { group: raw.group } : {},
|
|
115
|
+
...typeof raw.order === "number" && Number.isFinite(raw.order) ? { order: raw.order } : {},
|
|
116
|
+
...typeof raw.locked === "boolean" ? { locked: raw.locked } : {}
|
|
117
|
+
};
|
|
118
|
+
return Object.keys(ui).length > 0 ? ui : void 0;
|
|
119
|
+
}
|
|
120
|
+
function isSensitiveField(field) {
|
|
121
|
+
return field.sensitive === true || field._meta?.sensitive === true;
|
|
122
|
+
}
|
|
123
|
+
function getFieldReadPermission(field) {
|
|
124
|
+
if (typeof field.readPermission === "string") return field.readPermission;
|
|
125
|
+
const metaPermission = field._meta?.readPermission;
|
|
126
|
+
return typeof metaPermission === "string" ? metaPermission : void 0;
|
|
127
|
+
}
|
|
128
|
+
function isTransientField(field) {
|
|
129
|
+
return field.transient === true || field._meta?.transient === true;
|
|
130
|
+
}
|
|
131
|
+
function isPolicyAddressableField(field) {
|
|
132
|
+
return field._meta?.__smrtSystemField !== true && field.type !== "oneToMany" && field.type !== "manyToMany" && field.type !== "meta";
|
|
133
|
+
}
|
|
134
|
+
function isRequiredField(field) {
|
|
135
|
+
if (field.nullable === true || field._meta?.nullable === true) return false;
|
|
136
|
+
return field.required === true || field._meta?.required === true;
|
|
137
|
+
}
|
|
138
|
+
function getCodeDefault(field) {
|
|
139
|
+
if (field.default !== void 0) return { value: field.default };
|
|
140
|
+
if (field._meta?.default !== void 0) return { value: field._meta.default };
|
|
141
|
+
}
|
|
142
|
+
function isUsableRequiredDefault(defaultBox) {
|
|
143
|
+
if (!defaultBox) return false;
|
|
144
|
+
return defaultBox.value !== null && defaultBox.value !== "";
|
|
145
|
+
}
|
|
146
|
+
function buildCodeSeedVisibility(fields) {
|
|
147
|
+
let hasBasicMarkers = false;
|
|
148
|
+
const hints = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const [name, field] of fields) {
|
|
150
|
+
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
151
|
+
hints.set(name, ui);
|
|
152
|
+
if (ui?.basic === true) hasBasicMarkers = true;
|
|
153
|
+
}
|
|
154
|
+
const visibility = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const [name] of fields) {
|
|
156
|
+
const ui = hints.get(name);
|
|
157
|
+
if (ui?.basic === true) visibility.set(name, "basic");
|
|
158
|
+
else if (ui?.basic === false || hasBasicMarkers) visibility.set(name, "advanced");
|
|
159
|
+
else visibility.set(name, "basic");
|
|
160
|
+
}
|
|
161
|
+
return visibility;
|
|
162
|
+
}
|
|
163
|
+
function buildCodeSeedDelta(field, visibility) {
|
|
164
|
+
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
165
|
+
const help = typeof field.description === "string" ? field.description : typeof field._meta?.description === "string" ? field._meta.description : void 0;
|
|
166
|
+
const codeDefault = getCodeDefault(field);
|
|
167
|
+
return {
|
|
168
|
+
...codeDefault ? { default: codeDefault } : {},
|
|
169
|
+
visibility,
|
|
170
|
+
...help !== void 0 ? { help } : {},
|
|
171
|
+
...ui?.order !== void 0 ? { order: ui.order } : {},
|
|
172
|
+
...ui?.locked === true ? { locked: true } : {}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function getCodeSeedGroup(field) {
|
|
176
|
+
return sanitizeFieldUIHints(field._meta?.ui)?.group ?? null;
|
|
177
|
+
}
|
|
178
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
179
|
+
function usesTextIdStorage(field) {
|
|
180
|
+
return field.idType === "text" || field._meta?.idType === "text";
|
|
181
|
+
}
|
|
182
|
+
function isStorableReferenceId(field, value) {
|
|
183
|
+
if (typeof value !== "string" || value === "") return false;
|
|
184
|
+
return usesTextIdStorage(field) || UUID_PATTERN.test(value);
|
|
185
|
+
}
|
|
186
|
+
function assertDefaultValueMatchesFieldType(objectRef, fieldName, field, value) {
|
|
187
|
+
const label = `Field policy default for "${objectRef}.${fieldName}"`;
|
|
188
|
+
if (value === null) {
|
|
189
|
+
if (isRequiredField(field)) throw new Error(`${label} may not be null: the field is required, so a null default is never a usable default`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
switch (field.type) {
|
|
193
|
+
case "text":
|
|
194
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string (field type "text")`);
|
|
195
|
+
return;
|
|
196
|
+
case "integer":
|
|
197
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer (field type "integer")`);
|
|
198
|
+
return;
|
|
199
|
+
case "decimal":
|
|
200
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number (field type "decimal")`);
|
|
201
|
+
return;
|
|
202
|
+
case "boolean":
|
|
203
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean (field type "boolean")`);
|
|
204
|
+
return;
|
|
205
|
+
case "datetime":
|
|
206
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) throw new Error(`${label} must be a date-parseable string (field type "datetime")`);
|
|
207
|
+
return;
|
|
208
|
+
case "json": return;
|
|
209
|
+
case "foreignKey":
|
|
210
|
+
case "crossPackageRef":
|
|
211
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string id (field type "${field.type}")`);
|
|
212
|
+
if (!isStorableReferenceId(field, value)) throw new Error(`${label} must be a UUID string (the reference column stores native UUIDs; declare idType 'text' on the field to store arbitrary ids)`);
|
|
213
|
+
return;
|
|
214
|
+
default: throw new Error(`${label} is not supported: defaults cannot target "${String(field.type)}" fields`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/models/FieldPolicy.ts
|
|
219
|
+
var __defProp$1 = Object.defineProperty;
|
|
220
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
221
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
222
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
223
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
224
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
227
|
+
ensureFieldPolicyPermissionsRegistered();
|
|
228
|
+
var FIELD_POLICY_SCOPE_DENIAL_MESSAGE = "Field policy scope is not permitted for this principal.";
|
|
229
|
+
function getFieldPolicyAmbientContext() {
|
|
230
|
+
const tenantContext = getCurrentTenant();
|
|
231
|
+
if (tenantContext) return {
|
|
232
|
+
permissions: tenantContext.permissions,
|
|
233
|
+
superAdminBypass: isSuperAdminBypass(),
|
|
234
|
+
tenantId: tenantContext.tenantId,
|
|
235
|
+
userId: tenantContext.userId
|
|
236
|
+
};
|
|
237
|
+
const sessionContext = getCurrentSessionPermissionContext();
|
|
238
|
+
if (!sessionContext?.tenantId) return;
|
|
239
|
+
return {
|
|
240
|
+
permissions: sessionContext.permissionSet,
|
|
241
|
+
superAdminBypass: sessionContext.superAdminBypass,
|
|
242
|
+
tenantId: sessionContext.tenantId,
|
|
243
|
+
userId: sessionContext.userId ?? void 0
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
var FieldPolicyAuthorizationError = class extends Error {
|
|
247
|
+
code = "permission_denied";
|
|
248
|
+
status = 403;
|
|
249
|
+
constructor(message) {
|
|
250
|
+
super(message);
|
|
251
|
+
this.name = "FieldPolicyAuthorizationError";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var FieldPolicyTenantIsolationError = class extends TenantIsolationError {
|
|
255
|
+
publicMessage = FIELD_POLICY_SCOPE_DENIAL_MESSAGE;
|
|
256
|
+
status = 403;
|
|
257
|
+
constructor(diagnosticMessage, details) {
|
|
258
|
+
super(FIELD_POLICY_SCOPE_DENIAL_MESSAGE);
|
|
259
|
+
Object.defineProperty(this, "cause", {
|
|
260
|
+
configurable: true,
|
|
261
|
+
enumerable: false,
|
|
262
|
+
value: new Error(diagnosticMessage),
|
|
263
|
+
writable: false
|
|
264
|
+
});
|
|
265
|
+
Object.defineProperty(this, "diagnostic", {
|
|
266
|
+
configurable: true,
|
|
267
|
+
enumerable: false,
|
|
268
|
+
value: {
|
|
269
|
+
...details,
|
|
270
|
+
message: diagnosticMessage
|
|
271
|
+
},
|
|
272
|
+
writable: false
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
var FieldPolicy = class extends SmrtObject {
|
|
277
|
+
objectRef = "";
|
|
278
|
+
fieldName = "";
|
|
279
|
+
scopeType = "app";
|
|
280
|
+
tenantId = null;
|
|
281
|
+
userId = null;
|
|
282
|
+
scopeKey = "";
|
|
283
|
+
defaultValue = null;
|
|
284
|
+
visibility = null;
|
|
285
|
+
help = null;
|
|
286
|
+
label = null;
|
|
287
|
+
displayOrder = null;
|
|
288
|
+
locked = null;
|
|
289
|
+
updatedBy = null;
|
|
290
|
+
constructor(options = {}) {
|
|
291
|
+
super(options);
|
|
292
|
+
if (options.objectRef !== void 0) this.objectRef = options.objectRef;
|
|
293
|
+
if (options.fieldName !== void 0) this.fieldName = options.fieldName;
|
|
294
|
+
if (options.scopeType !== void 0) this.scopeType = options.scopeType;
|
|
295
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
296
|
+
if (options.userId !== void 0) this.userId = options.userId;
|
|
297
|
+
if (options.defaultValue !== void 0 && options.defaultValueRaw !== void 0) throw new Error("FieldPolicy accepts either defaultValue (already JSON-encoded) or defaultValueRaw (a plain value to serialize), not both");
|
|
298
|
+
if (options.defaultValueRaw !== void 0) this.setDefaultValue(options.defaultValueRaw);
|
|
299
|
+
else if (options.defaultValue !== void 0) this.defaultValue = options.defaultValue;
|
|
300
|
+
if (options.visibility !== void 0) this.visibility = options.visibility;
|
|
301
|
+
if (options.help !== void 0) this.help = options.help;
|
|
302
|
+
if (options.label !== void 0) this.label = options.label;
|
|
303
|
+
if (options.displayOrder !== void 0) this.displayOrder = options.displayOrder;
|
|
304
|
+
if (options.locked !== void 0) this.locked = options.locked;
|
|
305
|
+
if (options.updatedBy !== void 0) this.updatedBy = options.updatedBy;
|
|
306
|
+
}
|
|
307
|
+
/** Parse the stored JSON default. `undefined` = no stored default (inherit). */
|
|
308
|
+
getDefaultValue() {
|
|
309
|
+
if (this.defaultValue === null || this.defaultValue === void 0) return;
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse(this.defaultValue);
|
|
312
|
+
} catch {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Serialize a default value; `undefined` clears the override (inherit). */
|
|
317
|
+
setDefaultValue(value) {
|
|
318
|
+
this.defaultValue = value === void 0 ? null : JSON.stringify(value);
|
|
319
|
+
}
|
|
320
|
+
async save() {
|
|
321
|
+
this.attributeScopeToAmbientContext();
|
|
322
|
+
const previousIdentity = await this.getPersistedIdentity();
|
|
323
|
+
if (previousIdentity) {
|
|
324
|
+
this.assertScopeOwnedByAmbientContext(previousIdentity, "save");
|
|
325
|
+
await this.assertScopePermission(previousIdentity);
|
|
326
|
+
}
|
|
327
|
+
this.assertScopeOwnedByAmbientContext(this, "save");
|
|
328
|
+
await this.assertScopePermission(this);
|
|
329
|
+
const ambientContext = getFieldPolicyAmbientContext();
|
|
330
|
+
if (ambientContext) this.updatedBy = ambientContext.userId ?? null;
|
|
331
|
+
this.normalizeDefaultValueForPersistence();
|
|
332
|
+
await this.validateFieldPolicy(previousIdentity);
|
|
333
|
+
this.scopeKey = this.computeScopeKey();
|
|
334
|
+
const identityChanged = previousIdentity && (previousIdentity.objectRef !== this.objectRef || previousIdentity.fieldName !== this.fieldName || previousIdentity.scopeType !== this.scopeType || previousIdentity.scopeKey !== this.scopeKey);
|
|
335
|
+
const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange() : await super.save();
|
|
336
|
+
if (identityChanged && previousIdentity) invalidateFieldPolicyCache(previousIdentity.objectRef, this.db);
|
|
337
|
+
invalidateFieldPolicyCache(this.objectRef, this.db);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
async saveAfterIdentityChange() {
|
|
341
|
+
if (typeof this.db.beginTransaction === "function") return this.saveAfterIdentityChangeInTransaction();
|
|
342
|
+
return this.saveAfterIdentityChangeWithDeferredDelete();
|
|
343
|
+
}
|
|
344
|
+
async saveAfterIdentityChangeInTransaction() {
|
|
345
|
+
const originalDb = this._db;
|
|
346
|
+
const originalOptionsDb = this.options.db;
|
|
347
|
+
const tx = await this.db.beginTransaction?.();
|
|
348
|
+
if (!tx) return this.saveAfterIdentityChangeWithDeferredDelete();
|
|
349
|
+
try {
|
|
350
|
+
this._db = tx;
|
|
351
|
+
this.options.db = tx;
|
|
352
|
+
await super.delete();
|
|
353
|
+
const result = await super.save();
|
|
354
|
+
await tx.commit();
|
|
355
|
+
return result;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
try {
|
|
358
|
+
await tx.rollback();
|
|
359
|
+
} catch {}
|
|
360
|
+
throw error;
|
|
361
|
+
} finally {
|
|
362
|
+
this._db = originalDb;
|
|
363
|
+
this.options.db = originalOptionsDb;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async saveAfterIdentityChangeWithDeferredDelete() {
|
|
367
|
+
const previousId = this.id;
|
|
368
|
+
if (!previousId) return super.save();
|
|
369
|
+
const replacementId = crypto.randomUUID();
|
|
370
|
+
let replacementSaved = false;
|
|
371
|
+
this.id = replacementId;
|
|
372
|
+
try {
|
|
373
|
+
const result = await super.save();
|
|
374
|
+
replacementSaved = true;
|
|
375
|
+
await this.db.delete(this.tableName, { id: previousId });
|
|
376
|
+
return result;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (replacementSaved) try {
|
|
379
|
+
await this.db.delete(this.tableName, { id: replacementId });
|
|
380
|
+
} catch {}
|
|
381
|
+
this.id = previousId;
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async delete() {
|
|
386
|
+
const persisted = await this.getPersistedIdentity();
|
|
387
|
+
if (persisted) this.assertScopeOwnedByAmbientContext(persisted, "delete");
|
|
388
|
+
await this.assertScopePermission(persisted ?? this);
|
|
389
|
+
const objectRef = persisted?.objectRef ?? this.objectRef;
|
|
390
|
+
await super.delete();
|
|
391
|
+
invalidateFieldPolicyCache(objectRef, this.db);
|
|
392
|
+
if (this.objectRef && this.objectRef !== objectRef) invalidateFieldPolicyCache(this.objectRef, this.db);
|
|
393
|
+
}
|
|
394
|
+
async validateFieldPolicy(previousIdentity) {
|
|
395
|
+
if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldPolicy.objectRef is required");
|
|
396
|
+
if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldPolicy.fieldName is required");
|
|
397
|
+
if (!FIELD_POLICY_SCOPE_TYPES.includes(this.scopeType)) throw new Error(`FieldPolicy.scopeType must be one of ${FIELD_POLICY_SCOPE_TYPES.join(", ")}; got "${this.scopeType}"`);
|
|
398
|
+
if (this.visibility !== null && !FIELD_POLICY_VISIBILITIES.includes(this.visibility)) throw new Error(`FieldPolicy.visibility must be null or one of ${FIELD_POLICY_VISIBILITIES.join(", ")}; got "${this.visibility}"`);
|
|
399
|
+
if (this.displayOrder !== null && (typeof this.displayOrder !== "number" || !Number.isInteger(this.displayOrder))) throw new Error("FieldPolicy.displayOrder must be null or an integer");
|
|
400
|
+
this.validateScopeConsistency();
|
|
401
|
+
this.validateTenantContextBoundary();
|
|
402
|
+
const fieldDef = (await getObjectFieldMap(this.objectRef)).get(this.fieldName);
|
|
403
|
+
if (!fieldDef) throw new Error(`Unknown field "${this.fieldName}" on "${this.objectRef}"`);
|
|
404
|
+
if (fieldDef._meta?.__smrtSystemField === true) throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is a framework system field and is not policy-addressable`);
|
|
405
|
+
if (fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is a relationship pseudo-field and is not policy-addressable`);
|
|
406
|
+
if (fieldDef.type === "meta") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is STI meta storage and is not policy-addressable`);
|
|
407
|
+
if (this.locked !== null && this.scopeType === "user") throw new Error("FieldPolicy.locked may only be set on org rows (app or tenant scope)");
|
|
408
|
+
if (this.defaultValue !== null) {
|
|
409
|
+
this.validateDefaultAgainstSecurityRail(fieldDef);
|
|
410
|
+
const parsed = this.parseDefaultValueOrThrow();
|
|
411
|
+
assertDefaultValueMatchesFieldType(this.objectRef, this.fieldName, fieldDef, parsed);
|
|
412
|
+
}
|
|
413
|
+
const demotesRequiredField = (this.visibility === "advanced" || this.visibility === "hidden") && isRequiredField(fieldDef);
|
|
414
|
+
const ownDefault = demotesRequiredField && this.defaultValue !== null ? { value: this.parseDefaultValueOrThrow() } : void 0;
|
|
415
|
+
const needsOrgDefault = demotesRequiredField && !isUsableRequiredDefault(ownDefault);
|
|
416
|
+
const needsLockCheck = this.scopeType === "user";
|
|
417
|
+
if (needsOrgDefault || needsLockCheck) {
|
|
418
|
+
const orgPolicy = await this.resolveOrgTierFieldPolicy(previousIdentity);
|
|
419
|
+
if (needsOrgDefault) {
|
|
420
|
+
if (!isUsableRequiredDefault(orgPolicy?.hasDefault ? { value: orgPolicy.defaultValue } : void 0)) throw new Error(`Cannot set visibility "${this.visibility}" for required field "${this.objectRef}.${this.fieldName}": no resolved default exists at or below this layer`);
|
|
421
|
+
}
|
|
422
|
+
if (needsLockCheck && orgPolicy?.locked) throw new FieldPolicyAuthorizationError(`Field "${this.objectRef}.${this.fieldName}" is locked by org policy; user-scope overrides are not allowed`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Effective org-tier (code → app → tenant hierarchy) policy for this row's
|
|
427
|
+
* field, computed by the RESOLVER so write-time checks share the one
|
|
428
|
+
* precedence implementation — including ancestor-tenant cascades via the
|
|
429
|
+
* default hierarchy loader. The chain tenant is the row's own tenant for
|
|
430
|
+
* tenant-scope rows, the ambient context tenant for user-scope rows, and
|
|
431
|
+
* none for app-scope rows (their only lower layer is the code seed).
|
|
432
|
+
*
|
|
433
|
+
* During an update, the persisted row is excluded from the lower-layer
|
|
434
|
+
* projection. That makes a clear-default + required-field demotion validate
|
|
435
|
+
* the post-write policy rather than accidentally counting the row being
|
|
436
|
+
* replaced.
|
|
437
|
+
*/
|
|
438
|
+
async resolveOrgTierFieldPolicy(previousIdentity) {
|
|
439
|
+
const tenantIdForChain = this.scopeType === "tenant" ? this.tenantId : this.scopeType === "user" ? getFieldPolicyAmbientContext()?.tenantId ?? null : null;
|
|
440
|
+
const { resolveFieldPolicy } = await import("../index.js").then((n) => n.t);
|
|
441
|
+
return (await resolveFieldPolicy(this.objectRef, {
|
|
442
|
+
tenantId: tenantIdForChain,
|
|
443
|
+
db: this.options.db ?? this.options.persistence,
|
|
444
|
+
...previousIdentity ? { excludePolicyIds: /* @__PURE__ */ new Set([previousIdentity.id]) } : {}
|
|
445
|
+
})).fields[this.fieldName];
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Exactly-one-owner scope shape: app rows carry neither id, tenant rows
|
|
449
|
+
* carry only `tenantId`, user rows carry only `userId` (the user tier is
|
|
450
|
+
* keyed by user alone so preferences follow the user across tenants).
|
|
451
|
+
*/
|
|
452
|
+
validateScopeConsistency() {
|
|
453
|
+
if (this.scopeType === "app") {
|
|
454
|
+
if (this.tenantId !== null || this.userId !== null) throw new Error("App-scope field policy rows must have tenantId and userId null");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (this.scopeType === "tenant") {
|
|
458
|
+
if (!this.tenantId || this.userId !== null) throw new Error("Tenant-scope field policy rows must set tenantId and leave userId null");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!this.userId || this.tenantId !== null) throw new Error("User-scope field policy rows must set userId and leave tenantId null");
|
|
462
|
+
}
|
|
463
|
+
/** `userId ?? tenantId ?? '__app__'` — the `conflictColumns` scope key. */
|
|
464
|
+
computeScopeKey() {
|
|
465
|
+
return this.userId ?? this.tenantId ?? "__app__";
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Derive a scope-owner column the transport could not carry (#2047).
|
|
469
|
+
*
|
|
470
|
+
* Core's mass-assignment guard treats `tenantId` as server-managed and
|
|
471
|
+
* strips it from EVERY generated create/update body, and `FieldPolicy` is
|
|
472
|
+
* deliberately not `@TenantScoped`, so the tenancy interceptor never
|
|
473
|
+
* repopulates it. A `POST {scopeType:'tenant', tenantId}` therefore always
|
|
474
|
+
* reached `validateScopeConsistency` with `tenantId === null`, making the
|
|
475
|
+
* org tier write-dead over every generated surface — the model, as the
|
|
476
|
+
* single validation authority for these rows, fills it in instead.
|
|
477
|
+
*
|
|
478
|
+
* This grants nothing: `assertScopeOwnedByAmbientContext` already requires
|
|
479
|
+
* a non-bypass caller's tenant/user rows to name exactly the ambient
|
|
480
|
+
* tenant/user, so the derived value is the ONLY value that could ever have
|
|
481
|
+
* been accepted. An explicit value is never overwritten (a super-admin
|
|
482
|
+
* bypass caller keeps naming other scopes), and with neither tenancy nor
|
|
483
|
+
* authenticated-session context nothing is derived — scope-shape validation
|
|
484
|
+
* rejects the row as before.
|
|
485
|
+
* Consequence: a bypass caller writing ANOTHER tenant's row must pass
|
|
486
|
+
* `tenantId` through a server-side model call, because the generated routes
|
|
487
|
+
* still strip it.
|
|
488
|
+
*/
|
|
489
|
+
attributeScopeToAmbientContext() {
|
|
490
|
+
const context = getFieldPolicyAmbientContext();
|
|
491
|
+
if (!context) return;
|
|
492
|
+
if (this.scopeType === "tenant" && this.tenantId === null) this.tenantId = context.tenantId;
|
|
493
|
+
if (this.scopeType === "user" && this.userId === null) this.userId = context.userId ?? null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Fail-closed write boundary against the ambient tenant context, applied to
|
|
497
|
+
* the NEW scope on save and to the PERSISTED scope on save/delete of an
|
|
498
|
+
* existing row.
|
|
499
|
+
*
|
|
500
|
+
* With NO ambient identity at all (neither tenant ALS nor authenticated
|
|
501
|
+
* session-permission ALS), only app-scope rows are accepted: tenant- and
|
|
502
|
+
* user-scope rows are unattributable without a context, so they are
|
|
503
|
+
* rejected outright rather than allowed by default. Inside a non-bypass
|
|
504
|
+
* context, a caller may only touch rows for its own tenant, and user rows
|
|
505
|
+
* only for its own user id — a context that carries NO user id may not
|
|
506
|
+
* touch the user tier at all.
|
|
507
|
+
* App-wide rows then require super-admin bypass (or a context-less/system
|
|
508
|
+
* caller).
|
|
509
|
+
*
|
|
510
|
+
* Package rule: a missing identity component DENIES, it never skips.
|
|
511
|
+
*/
|
|
512
|
+
assertScopeOwnedByAmbientContext(scope, operation) {
|
|
513
|
+
const context = getFieldPolicyAmbientContext();
|
|
514
|
+
if (!context) {
|
|
515
|
+
if (scope.scopeType === "tenant" || scope.scopeType === "user") throw new FieldPolicyTenantIsolationError(`${scope.scopeType === "tenant" ? "Tenant" : "User"}-scope field policy ${operation}s require an ambient tenant context (or super-admin bypass); without one the caller identity is unattributable`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (context.superAdminBypass) return;
|
|
519
|
+
if (scope.scopeType === "app") throw new FieldPolicyTenantIsolationError(`App-scope field policy ${operation}s are not allowed inside a tenant context (use a system context or super-admin bypass)`, { tenantId: context.tenantId });
|
|
520
|
+
if (scope.scopeType === "tenant" && scope.tenantId !== context.tenantId) throw new FieldPolicyTenantIsolationError(`Tenant isolation violation in FieldPolicy.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${scope.tenantId}'`, {
|
|
521
|
+
tenantId: context.tenantId,
|
|
522
|
+
attemptedTenantId: scope.tenantId ?? void 0
|
|
523
|
+
});
|
|
524
|
+
if (scope.scopeType === "user") {
|
|
525
|
+
if (context.userId === void 0) throw new FieldPolicyTenantIsolationError(`User-scope field policy ${operation}s require an ambient context that carries a user id; this context cannot attribute a user-scope write`, { tenantId: context.tenantId });
|
|
526
|
+
if (scope.userId !== context.userId) throw new FieldPolicyTenantIsolationError(`Tenant isolation violation in FieldPolicy.${operation}: context user is '${context.userId}' but the row belongs to '${scope.userId}'`, { tenantId: context.tenantId });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
validateTenantContextBoundary() {
|
|
530
|
+
this.assertScopeOwnedByAmbientContext({
|
|
531
|
+
scopeType: this.scopeType,
|
|
532
|
+
tenantId: this.tenantId,
|
|
533
|
+
userId: this.userId
|
|
534
|
+
}, "save");
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Permission boundary for policy mutations. Scope ownership is checked
|
|
538
|
+
* separately (and first) because permission never grants cross-tenant or
|
|
539
|
+
* cross-user access. Existing rows are checked against their persisted
|
|
540
|
+
* scope before an identity-changing save can mutate or delete them.
|
|
541
|
+
*/
|
|
542
|
+
async assertScopePermission(scope) {
|
|
543
|
+
const context = getFieldPolicyAmbientContext();
|
|
544
|
+
const [, , operation] = (scope.scopeType === "user" ? PERSONALIZE_FIELD_POLICY_PERMISSION : MANAGE_FIELD_POLICY_PERMISSION).split(".");
|
|
545
|
+
await assertOperationPermission({
|
|
546
|
+
collection: "fields.policy",
|
|
547
|
+
action: operation ?? "manage",
|
|
548
|
+
db: this.options.db ?? this.options.persistence,
|
|
549
|
+
tenantId: context?.tenantId ?? null,
|
|
550
|
+
userId: context?.userId ?? null,
|
|
551
|
+
permissionSet: context?.permissions
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
validateDefaultAgainstSecurityRail(fieldDef) {
|
|
555
|
+
if (isTransientField(fieldDef)) throw new Error(`Cannot store a default for transient field "${this.objectRef}.${this.fieldName}"`);
|
|
556
|
+
if (isSensitiveField(fieldDef)) throw new Error(`Cannot store a default for sensitive field "${this.objectRef}.${this.fieldName}"`);
|
|
557
|
+
const readPermission = getFieldReadPermission(fieldDef);
|
|
558
|
+
if (readPermission) throw new Error(`Cannot store a default for read-permission-gated field "${this.objectRef}.${this.fieldName}" (requires "${readPermission}")`);
|
|
559
|
+
}
|
|
560
|
+
parseDefaultValueOrThrow() {
|
|
561
|
+
try {
|
|
562
|
+
return JSON.parse(this.defaultValue);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
throw new Error(`FieldPolicy default for "${this.objectRef}.${this.fieldName}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}. The "defaultValue" option is the already-encoded channel; pass a plain value as "defaultValueRaw" (or call setDefaultValue()).`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* The persisted row this save/delete would replace, looked up by primary
|
|
569
|
+
* key and — when that misses — by the NATURAL key.
|
|
570
|
+
*
|
|
571
|
+
* The natural-key fallback is load-bearing for authorization: every
|
|
572
|
+
* generated create arrives with a freshly minted UUID, so a primary-key
|
|
573
|
+
* lookup always misses, yet the `conflictColumns` upsert still overwrites
|
|
574
|
+
* whatever row already occupies `(objectRef, fieldName, scopeType,
|
|
575
|
+
* scopeKey)`. Authorizing on the primary key alone therefore skipped the
|
|
576
|
+
* persisted-scope guard on exactly the path that can replace an existing
|
|
577
|
+
* row's contents.
|
|
578
|
+
*/
|
|
579
|
+
async getPersistedIdentity() {
|
|
580
|
+
const existing = await this.getPersistedRowById() ?? await this.getPersistedRowByNaturalKey();
|
|
581
|
+
if (!existing) return null;
|
|
582
|
+
const row = existing;
|
|
583
|
+
const read = (camel, snake, fallback) => {
|
|
584
|
+
if (row[camel] !== void 0 && row[camel] !== null) return String(row[camel]);
|
|
585
|
+
if (row[snake] !== void 0 && row[snake] !== null) return String(row[snake]);
|
|
586
|
+
return fallback;
|
|
587
|
+
};
|
|
588
|
+
const readNullable = (camel, snake) => {
|
|
589
|
+
const value = row[camel] !== void 0 ? row[camel] : row[snake];
|
|
590
|
+
return value === void 0 || value === null ? null : String(value);
|
|
591
|
+
};
|
|
592
|
+
return {
|
|
593
|
+
id: read("id", "id", String(this.id ?? "")),
|
|
594
|
+
objectRef: read("objectRef", "object_ref", this.objectRef),
|
|
595
|
+
fieldName: read("fieldName", "field_name", this.fieldName),
|
|
596
|
+
scopeType: read("scopeType", "scope_type", this.scopeType),
|
|
597
|
+
scopeKey: read("scopeKey", "scope_key", this.scopeKey),
|
|
598
|
+
tenantId: readNullable("tenantId", "tenant_id"),
|
|
599
|
+
userId: readNullable("userId", "user_id")
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async getPersistedRowById() {
|
|
603
|
+
if (!this.id) return null;
|
|
604
|
+
return await this.db.get(this.tableName, { id: this.id }) ?? null;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The row occupying this row's `conflictColumns` tuple, if any. Column
|
|
608
|
+
* names are the physical snake_case ones: this reads the driver directly
|
|
609
|
+
* rather than through a collection (the model has no collection handle).
|
|
610
|
+
*/
|
|
611
|
+
async getPersistedRowByNaturalKey() {
|
|
612
|
+
if (!this.objectRef || !this.fieldName) return null;
|
|
613
|
+
return await this.db.get(this.tableName, {
|
|
614
|
+
object_ref: this.objectRef,
|
|
615
|
+
field_name: this.fieldName,
|
|
616
|
+
scope_type: this.scopeType,
|
|
617
|
+
scope_key: this.computeScopeKey()
|
|
618
|
+
}) ?? null;
|
|
619
|
+
}
|
|
620
|
+
normalizeDefaultValueForPersistence() {
|
|
621
|
+
const raw = this.defaultValue;
|
|
622
|
+
if (raw === null || raw === void 0) {
|
|
623
|
+
this.defaultValue = null;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (typeof raw === "string") {
|
|
627
|
+
this.defaultValue = raw;
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
this.defaultValue = JSON.stringify(raw);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
__decorateClass$1([field({ required: true })], FieldPolicy.prototype, "objectRef", 2);
|
|
634
|
+
__decorateClass$1([field({ required: true })], FieldPolicy.prototype, "fieldName", 2);
|
|
635
|
+
__decorateClass$1([field({ required: true })], FieldPolicy.prototype, "scopeType", 2);
|
|
636
|
+
__decorateClass$1([tenantId({ nullable: true })], FieldPolicy.prototype, "tenantId", 2);
|
|
637
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "userId", 2);
|
|
638
|
+
__decorateClass$1([field({
|
|
639
|
+
type: "text",
|
|
640
|
+
required: true
|
|
641
|
+
})], FieldPolicy.prototype, "scopeKey", 2);
|
|
642
|
+
__decorateClass$1([field({
|
|
643
|
+
type: "text",
|
|
644
|
+
nullable: true
|
|
645
|
+
})], FieldPolicy.prototype, "defaultValue", 2);
|
|
646
|
+
__decorateClass$1([field({
|
|
647
|
+
type: "text",
|
|
648
|
+
nullable: true
|
|
649
|
+
})], FieldPolicy.prototype, "visibility", 2);
|
|
650
|
+
__decorateClass$1([field({
|
|
651
|
+
type: "text",
|
|
652
|
+
nullable: true
|
|
653
|
+
})], FieldPolicy.prototype, "help", 2);
|
|
654
|
+
__decorateClass$1([field({
|
|
655
|
+
type: "text",
|
|
656
|
+
nullable: true
|
|
657
|
+
})], FieldPolicy.prototype, "label", 2);
|
|
658
|
+
__decorateClass$1([field({
|
|
659
|
+
type: "integer",
|
|
660
|
+
nullable: true
|
|
661
|
+
})], FieldPolicy.prototype, "displayOrder", 2);
|
|
662
|
+
__decorateClass$1([field({
|
|
663
|
+
type: "boolean",
|
|
664
|
+
nullable: true
|
|
665
|
+
})], FieldPolicy.prototype, "locked", 2);
|
|
666
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "updatedBy", 2);
|
|
667
|
+
FieldPolicy = __decorateClass$1([smrt({
|
|
668
|
+
tableName: "_smrt_field_policies",
|
|
669
|
+
conflictColumns: [
|
|
670
|
+
"object_ref",
|
|
671
|
+
"field_name",
|
|
672
|
+
"scope_type",
|
|
673
|
+
"scope_key"
|
|
674
|
+
],
|
|
675
|
+
api: {
|
|
676
|
+
include: [
|
|
677
|
+
"create",
|
|
678
|
+
"update",
|
|
679
|
+
"delete"
|
|
680
|
+
],
|
|
681
|
+
principalContext: true
|
|
682
|
+
},
|
|
683
|
+
cli: {
|
|
684
|
+
include: [
|
|
685
|
+
"create",
|
|
686
|
+
"update",
|
|
687
|
+
"delete"
|
|
688
|
+
],
|
|
689
|
+
exclude: ["getDefaultValue", "setDefaultValue"]
|
|
690
|
+
},
|
|
691
|
+
mcp: { include: [] }
|
|
692
|
+
})], FieldPolicy);
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/collections/FieldPolicyCollection.ts
|
|
695
|
+
var FieldPolicyCollection_exports = /* @__PURE__ */ __exportAll({
|
|
696
|
+
FieldPolicyCollection: () => FieldPolicyCollection,
|
|
697
|
+
MAX_FIELD_POLICY_AUDIT_COUNT_REFS: () => 200,
|
|
698
|
+
MAX_FIELD_POLICY_AUDIT_OBJECT_REFS: () => 100
|
|
699
|
+
});
|
|
700
|
+
var __defProp = Object.defineProperty;
|
|
701
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
702
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
703
|
+
enumerable: true,
|
|
704
|
+
configurable: true,
|
|
705
|
+
writable: true,
|
|
706
|
+
value
|
|
707
|
+
}) : obj[key] = value;
|
|
708
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
709
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
710
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
711
|
+
if (kind && result) __defProp(target, key, result);
|
|
712
|
+
return result;
|
|
713
|
+
};
|
|
714
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
715
|
+
var MAX_BATCH_OBJECT_REFS = 100;
|
|
716
|
+
MAX_BATCH_OBJECT_REFS * 2;
|
|
717
|
+
var FieldPolicyCollection = class extends SmrtCollection {
|
|
718
|
+
/**
|
|
719
|
+
* Permission-gated, tenant-scoped audit read for the defaults control
|
|
720
|
+
* panel. It returns editable own-tenant rows, read-only app summaries,
|
|
721
|
+
* projected user override counts, and identity-only manifest drift rows.
|
|
722
|
+
*/
|
|
723
|
+
async policyAudit(options = {}) {
|
|
724
|
+
const objectRefs = normalizeAuditRefs(options.objectRefs, "objectRefs", 100);
|
|
725
|
+
const countObjectRefs = normalizeAuditRefs(options.countObjectRefs ?? objectRefs, "countObjectRefs", 200);
|
|
726
|
+
const context = getCurrentTenant();
|
|
727
|
+
const tenantId = context?.tenantId ?? null;
|
|
728
|
+
const userId = context?.userId ?? null;
|
|
729
|
+
const permissionOptions = {
|
|
730
|
+
collection: "fields.policy",
|
|
731
|
+
db: this.db,
|
|
732
|
+
tenantId,
|
|
733
|
+
userId,
|
|
734
|
+
permissionSet: context?.permissions,
|
|
735
|
+
onDeny: "return"
|
|
736
|
+
};
|
|
737
|
+
const manage = tenantId ? await checkOperationPermission({
|
|
738
|
+
...permissionOptions,
|
|
739
|
+
action: "fields.policy.manage".split(".").at(-1) ?? "manage"
|
|
740
|
+
}) : { allowed: false };
|
|
741
|
+
const personalize = userId ? await checkOperationPermission({
|
|
742
|
+
...permissionOptions,
|
|
743
|
+
action: "fields.policy.personalize".split(".").at(-1) ?? "personalize"
|
|
744
|
+
}) : { allowed: false };
|
|
745
|
+
const caller = {
|
|
746
|
+
tenantId,
|
|
747
|
+
userId,
|
|
748
|
+
canManageOrg: manage.allowed,
|
|
749
|
+
canPersonalize: personalize.allowed && userId !== null
|
|
750
|
+
};
|
|
751
|
+
if (!caller.canManageOrg || tenantId === null) return emptyAudit(caller);
|
|
752
|
+
if (options.summaryOnly === true) return emptyAudit(caller);
|
|
753
|
+
const fieldMaps = /* @__PURE__ */ new Map();
|
|
754
|
+
const fieldMapFor = (objectRef) => {
|
|
755
|
+
let loaded = fieldMaps.get(objectRef);
|
|
756
|
+
if (!loaded) {
|
|
757
|
+
loaded = getObjectFieldMap(objectRef).catch(() => null);
|
|
758
|
+
fieldMaps.set(objectRef, loaded);
|
|
759
|
+
}
|
|
760
|
+
return loaded;
|
|
761
|
+
};
|
|
762
|
+
const classify = async (row) => {
|
|
763
|
+
const fields = await fieldMapFor(row.objectRef);
|
|
764
|
+
if (!fields) return "unknown-object";
|
|
765
|
+
const definition = fields.get(row.fieldName);
|
|
766
|
+
if (!definition) return "unknown-field";
|
|
767
|
+
if (isSensitiveField(definition) || getFieldReadPermission(definition) !== void 0 || isTransientField(definition)) return "gated";
|
|
768
|
+
return isPolicyAddressableField(definition) ? "ok" : "excluded-field";
|
|
769
|
+
};
|
|
770
|
+
const [appRows, orgRows, ownUserRows] = objectRefs.length ? await Promise.all([
|
|
771
|
+
this.list({ where: {
|
|
772
|
+
scopeType: "app",
|
|
773
|
+
"objectRef in": objectRefs
|
|
774
|
+
} }),
|
|
775
|
+
this.list({ where: {
|
|
776
|
+
scopeType: "tenant",
|
|
777
|
+
tenantId,
|
|
778
|
+
"objectRef in": objectRefs
|
|
779
|
+
} }),
|
|
780
|
+
userId ? this.list({ where: {
|
|
781
|
+
scopeType: "user",
|
|
782
|
+
userId,
|
|
783
|
+
"objectRef in": objectRefs
|
|
784
|
+
} }) : Promise.resolve([])
|
|
785
|
+
]) : [
|
|
786
|
+
[],
|
|
787
|
+
[],
|
|
788
|
+
[]
|
|
789
|
+
];
|
|
790
|
+
const audit = {
|
|
791
|
+
...emptyAudit(caller),
|
|
792
|
+
policies: options.countsOnly ? {} : await this.resolveAuditPolicies(objectRefs, tenantId, fieldMapFor)
|
|
793
|
+
};
|
|
794
|
+
const append = async (row, target, driftPrunable) => {
|
|
795
|
+
const state = await classify(row);
|
|
796
|
+
if (state === "ok") target.push(toAuditRow(row));
|
|
797
|
+
else if (state !== "gated") audit.driftRows.push({
|
|
798
|
+
...toDriftIdentity(row),
|
|
799
|
+
reason: state,
|
|
800
|
+
prunable: driftPrunable
|
|
801
|
+
});
|
|
802
|
+
};
|
|
803
|
+
for (const row of orgRows) await append(row, audit.orgRows, true);
|
|
804
|
+
for (const row of appRows) await append(row, audit.appRows, isSuperAdminBypass());
|
|
805
|
+
for (const row of ownUserRows) {
|
|
806
|
+
const state = await classify(row);
|
|
807
|
+
if (state !== "ok" && state !== "gated") audit.driftRows.push({
|
|
808
|
+
...toDriftIdentity(row),
|
|
809
|
+
reason: state,
|
|
810
|
+
prunable: caller.canPersonalize
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (options.includeDrift === true) {
|
|
814
|
+
const [driftAppRows, driftOrgRows, driftUserRows] = await Promise.all([
|
|
815
|
+
this.list({
|
|
816
|
+
where: { scopeType: "app" },
|
|
817
|
+
limit: 100
|
|
818
|
+
}),
|
|
819
|
+
this.list({
|
|
820
|
+
where: {
|
|
821
|
+
scopeType: "tenant",
|
|
822
|
+
tenantId
|
|
823
|
+
},
|
|
824
|
+
limit: 100
|
|
825
|
+
}),
|
|
826
|
+
userId ? this.list({
|
|
827
|
+
where: {
|
|
828
|
+
scopeType: "user",
|
|
829
|
+
userId
|
|
830
|
+
},
|
|
831
|
+
limit: 100
|
|
832
|
+
}) : Promise.resolve([])
|
|
833
|
+
]);
|
|
834
|
+
const knownIds = new Set([
|
|
835
|
+
...appRows,
|
|
836
|
+
...orgRows,
|
|
837
|
+
...ownUserRows
|
|
838
|
+
].map((row) => String(row.id)));
|
|
839
|
+
for (const [rows, prunable] of [
|
|
840
|
+
[driftAppRows, isSuperAdminBypass()],
|
|
841
|
+
[driftOrgRows, true],
|
|
842
|
+
[driftUserRows, caller.canPersonalize]
|
|
843
|
+
]) for (const row of rows) {
|
|
844
|
+
if (knownIds.has(String(row.id))) continue;
|
|
845
|
+
const state = await classify(row);
|
|
846
|
+
if (state !== "ok" && state !== "gated") audit.driftRows.push({
|
|
847
|
+
...toDriftIdentity(row),
|
|
848
|
+
reason: state,
|
|
849
|
+
prunable
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
const memberships = await MembershipCollection.create({ db: this.db });
|
|
854
|
+
const memberIds = [...new Set((await memberships.findActiveByTenant(tenantId)).map((membership) => membership.userId?.trim()).filter((id) => Boolean(id)))];
|
|
855
|
+
const projected = countObjectRefs.length && memberIds.length ? await this.list({
|
|
856
|
+
where: {
|
|
857
|
+
scopeType: "user",
|
|
858
|
+
"objectRef in": countObjectRefs,
|
|
859
|
+
"userId in": memberIds
|
|
860
|
+
},
|
|
861
|
+
select: ["objectRef", "fieldName"]
|
|
862
|
+
}) : [];
|
|
863
|
+
for (const row of projected) {
|
|
864
|
+
if (await classify(row) !== "ok") continue;
|
|
865
|
+
const byField = audit.userOverrideCounts[row.objectRef] ?? {};
|
|
866
|
+
audit.userOverrideCounts[row.objectRef] = byField;
|
|
867
|
+
byField[row.fieldName] = (byField[row.fieldName] ?? 0) + 1;
|
|
868
|
+
}
|
|
869
|
+
const { resolveSurvivingTenantChainIds } = await import("../index.js").then((n) => n.t);
|
|
870
|
+
const ancestorIds = (await resolveSurvivingTenantChainIds(tenantId, { db: this.db })).filter((id) => id !== tenantId);
|
|
871
|
+
if (ancestorIds.length && objectRefs.length) {
|
|
872
|
+
const ancestors = await this.list({ where: {
|
|
873
|
+
scopeType: "tenant",
|
|
874
|
+
"tenantId in": ancestorIds,
|
|
875
|
+
"objectRef in": objectRefs
|
|
876
|
+
} });
|
|
877
|
+
for (const row of ancestors) {
|
|
878
|
+
if (await classify(row) !== "ok") continue;
|
|
879
|
+
const names = audit.inheritedOrgKeys[row.objectRef] ?? [];
|
|
880
|
+
audit.inheritedOrgKeys[row.objectRef] = names;
|
|
881
|
+
if (!names.includes(row.fieldName)) names.push(row.fieldName);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
audit.orgRows.sort(sortAuditRows);
|
|
885
|
+
audit.appRows.sort(sortAuditRows);
|
|
886
|
+
audit.driftRows.sort(sortDriftRows);
|
|
887
|
+
for (const names of Object.values(audit.inheritedOrgKeys)) names.sort();
|
|
888
|
+
return audit;
|
|
889
|
+
}
|
|
890
|
+
async resolveAuditPolicies(objectRefs, tenantId, fieldMapFor) {
|
|
891
|
+
const { resolveFieldPolicyExplained } = await import("../index.js").then((n) => n.t);
|
|
892
|
+
const policies = {};
|
|
893
|
+
for (const objectRef of objectRefs) {
|
|
894
|
+
if (!await fieldMapFor(objectRef)) continue;
|
|
895
|
+
const resolved = await resolveFieldPolicyExplained(objectRef, {
|
|
896
|
+
tenantId,
|
|
897
|
+
db: this.db
|
|
898
|
+
});
|
|
899
|
+
policies[objectRef] = await this.gateExplainedForEditorResponse(resolved, {
|
|
900
|
+
manage: true,
|
|
901
|
+
personalize: false
|
|
902
|
+
}, true);
|
|
903
|
+
}
|
|
904
|
+
return policies;
|
|
905
|
+
}
|
|
906
|
+
/** All app-scope rows for an object, keyed by field name. */
|
|
907
|
+
async getAppRows(objectRef) {
|
|
908
|
+
const rows = await this.list({ where: {
|
|
909
|
+
objectRef,
|
|
910
|
+
scopeType: "app"
|
|
911
|
+
} });
|
|
912
|
+
const byField = /* @__PURE__ */ new Map();
|
|
913
|
+
for (const row of rows) byField.set(row.fieldName, row);
|
|
914
|
+
return byField;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* All tenant-scope rows for an object across a tenant chain, keyed
|
|
918
|
+
* `tenantId → fieldName → row`.
|
|
919
|
+
*/
|
|
920
|
+
async getTenantRows(objectRef, tenantIds) {
|
|
921
|
+
const byTenant = /* @__PURE__ */ new Map();
|
|
922
|
+
if (tenantIds.length === 0) return byTenant;
|
|
923
|
+
const rows = await this.list({ where: {
|
|
924
|
+
objectRef,
|
|
925
|
+
scopeType: "tenant",
|
|
926
|
+
"tenantId in": tenantIds
|
|
927
|
+
} });
|
|
928
|
+
for (const row of rows) {
|
|
929
|
+
if (!row.tenantId) continue;
|
|
930
|
+
let byField = byTenant.get(row.tenantId);
|
|
931
|
+
if (!byField) {
|
|
932
|
+
byField = /* @__PURE__ */ new Map();
|
|
933
|
+
byTenant.set(row.tenantId, byField);
|
|
934
|
+
}
|
|
935
|
+
byField.set(row.fieldName, row);
|
|
936
|
+
}
|
|
937
|
+
return byTenant;
|
|
938
|
+
}
|
|
939
|
+
/** All user-scope rows for an object and user, keyed by field name. */
|
|
940
|
+
async getUserRows(objectRef, userId) {
|
|
941
|
+
const rows = await this.list({ where: {
|
|
942
|
+
objectRef,
|
|
943
|
+
scopeType: "user",
|
|
944
|
+
userId
|
|
945
|
+
} });
|
|
946
|
+
const byField = /* @__PURE__ */ new Map();
|
|
947
|
+
for (const row of rows) byField.set(row.fieldName, row);
|
|
948
|
+
return byField;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Resolve merged field policy for a set of objectRefs for the CURRENT
|
|
952
|
+
* caller, gated for public consumption.
|
|
953
|
+
*
|
|
954
|
+
* Caller identity comes exclusively from the ambient tenant context
|
|
955
|
+
* (established by the app's auth hook, e.g. smrt-users' session context) —
|
|
956
|
+
* the request body cannot select another tenant or user (fail closed).
|
|
957
|
+
* Server-side consumers wanting explicit identities call
|
|
958
|
+
* `resolveFieldPolicy()` directly instead.
|
|
959
|
+
*
|
|
960
|
+
* Field gating mirrors the REST serializer's derivation (`sensitive` /
|
|
961
|
+
* `readPermission` read from both the top level and `_meta`): sensitive and
|
|
962
|
+
* read-permission-gated fields are ABSENT from the response for every
|
|
963
|
+
* caller (the generated action route cannot convey per-caller grants to
|
|
964
|
+
* this method, so gated fields fail closed), and transient fields are
|
|
965
|
+
* stripped for parity with generated client field definitions.
|
|
966
|
+
*/
|
|
967
|
+
async resolveBatch(options = {}) {
|
|
968
|
+
const rawRefs = options.objectRefs;
|
|
969
|
+
if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("resolveBatch requires a non-empty \"objectRefs\" string array");
|
|
970
|
+
if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("resolveBatch objectRefs must be non-empty strings");
|
|
971
|
+
const objectRefs = [...new Set(rawRefs)];
|
|
972
|
+
if (objectRefs.length > MAX_BATCH_OBJECT_REFS) throw new Error(`resolveBatch accepts at most ${MAX_BATCH_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
|
|
973
|
+
const context = getCurrentTenant();
|
|
974
|
+
const tenantId = context?.tenantId ?? null;
|
|
975
|
+
const userId = context?.userId ?? null;
|
|
976
|
+
const { resolveFieldPolicy } = await import("../index.js").then((n) => n.t);
|
|
977
|
+
const policies = {};
|
|
978
|
+
for (const objectRef of objectRefs) {
|
|
979
|
+
const resolved = await resolveFieldPolicy(objectRef, {
|
|
980
|
+
tenantId,
|
|
981
|
+
userId,
|
|
982
|
+
db: this.db
|
|
983
|
+
});
|
|
984
|
+
policies[objectRef] = await this.gateResolvedForPublicResponse(resolved);
|
|
985
|
+
}
|
|
986
|
+
return { policies };
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Context-derived bootstrap data for the policy gear. This is deliberately
|
|
990
|
+
* a collection action instead of reopening list/get: a caller can inspect
|
|
991
|
+
* only their safe effective policy and rows/layers they are permitted to
|
|
992
|
+
* edit, never arbitrary tenant or user policy rows.
|
|
993
|
+
*/
|
|
994
|
+
async getEditorState(options = {}) {
|
|
995
|
+
const objectRef = options.objectRef;
|
|
996
|
+
if (typeof objectRef !== "string" || objectRef.trim() === "") throw new Error("getEditorState requires a non-empty \"objectRef\" string");
|
|
997
|
+
const context = getCurrentTenant();
|
|
998
|
+
const permissionOptions = {
|
|
999
|
+
collection: "fields.policy",
|
|
1000
|
+
db: this.db,
|
|
1001
|
+
tenantId: context?.tenantId ?? null,
|
|
1002
|
+
userId: context?.userId ?? null,
|
|
1003
|
+
permissionSet: context?.permissions,
|
|
1004
|
+
onDeny: "return"
|
|
1005
|
+
};
|
|
1006
|
+
const manage = await checkOperationPermission({
|
|
1007
|
+
...permissionOptions,
|
|
1008
|
+
action: "fields.policy.manage".split(".").at(-1) ?? "manage"
|
|
1009
|
+
});
|
|
1010
|
+
const personalize = await checkOperationPermission({
|
|
1011
|
+
...permissionOptions,
|
|
1012
|
+
action: "fields.policy.personalize".split(".").at(-1) ?? "personalize"
|
|
1013
|
+
});
|
|
1014
|
+
const personalizeAllowed = personalize.allowed && typeof context?.userId === "string" && context.userId.length > 0;
|
|
1015
|
+
if (!manage.allowed && !personalizeAllowed) return {
|
|
1016
|
+
code: "permission_denied",
|
|
1017
|
+
message: `Permission denied for '${personalize.permission ?? "fields.policy.manage"}'.`,
|
|
1018
|
+
ok: false,
|
|
1019
|
+
status: 403
|
|
1020
|
+
};
|
|
1021
|
+
const { resolveFieldPolicyExplained } = await import("../index.js").then((n) => n.t);
|
|
1022
|
+
const resolved = await resolveFieldPolicyExplained(objectRef, {
|
|
1023
|
+
tenantId: context?.tenantId ?? null,
|
|
1024
|
+
userId: context?.userId ?? null,
|
|
1025
|
+
db: this.db
|
|
1026
|
+
});
|
|
1027
|
+
const policy = await this.gateExplainedForEditorResponse(resolved, {
|
|
1028
|
+
manage: manage.allowed,
|
|
1029
|
+
personalize: personalizeAllowed
|
|
1030
|
+
});
|
|
1031
|
+
const fieldNames = new Set(Object.keys(policy.fields));
|
|
1032
|
+
const personalLowerDefaultUsable = personalizeAllowed ? await this.getPersonalLowerDefaultUsability(objectRef, context?.tenantId ?? null, fieldNames) : {};
|
|
1033
|
+
const appRows = manage.allowed ? this.toEditorRows(await this.getAppRows(objectRef), fieldNames) : [];
|
|
1034
|
+
const tenantRows = manage.allowed && context?.tenantId ? this.toEditorRows((await this.getTenantRows(objectRef, [context.tenantId])).get(context.tenantId) ?? /* @__PURE__ */ new Map(), fieldNames) : [];
|
|
1035
|
+
const userRows = personalizeAllowed && context?.userId ? this.toEditorRows(await this.getUserRows(objectRef, context.userId), fieldNames) : [];
|
|
1036
|
+
return {
|
|
1037
|
+
capabilities: {
|
|
1038
|
+
manage: manage.allowed,
|
|
1039
|
+
personalize: personalizeAllowed
|
|
1040
|
+
},
|
|
1041
|
+
personalLowerDefaultUsable,
|
|
1042
|
+
policy,
|
|
1043
|
+
rows: {
|
|
1044
|
+
app: appRows,
|
|
1045
|
+
tenant: tenantRows,
|
|
1046
|
+
user: userRows
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Compute the non-disclosing fallback signal used only by personal drafts.
|
|
1052
|
+
* Passing no user id resolves exactly code/app/tenant precedence, including
|
|
1053
|
+
* the real tenant hierarchy, while keeping every lower-layer value server
|
|
1054
|
+
* side. The public map is limited to fields already admitted by the editor
|
|
1055
|
+
* response's security rail.
|
|
1056
|
+
*/
|
|
1057
|
+
async getPersonalLowerDefaultUsability(objectRef, tenantId, fieldNames) {
|
|
1058
|
+
const { resolveFieldPolicy } = await import("../index.js").then((n) => n.t);
|
|
1059
|
+
const lowerPolicy = await resolveFieldPolicy(objectRef, {
|
|
1060
|
+
tenantId,
|
|
1061
|
+
db: this.db
|
|
1062
|
+
});
|
|
1063
|
+
return Object.fromEntries(Array.from(fieldNames, (fieldName) => {
|
|
1064
|
+
const field = lowerPolicy.fields[fieldName];
|
|
1065
|
+
return [fieldName, isUsableRequiredDefault(field?.hasDefault ? { value: field.defaultValue } : void 0)];
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
async gateResolvedForPublicResponse(resolved) {
|
|
1069
|
+
const fieldMap = await getObjectFieldMap(resolved.objectRef);
|
|
1070
|
+
const fields = {};
|
|
1071
|
+
for (const [fieldName, policy] of Object.entries(resolved.fields)) {
|
|
1072
|
+
const fieldDef = fieldMap.get(fieldName);
|
|
1073
|
+
if (!fieldDef) continue;
|
|
1074
|
+
if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
|
|
1075
|
+
fields[fieldName] = policy;
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
objectRef: resolved.objectRef,
|
|
1079
|
+
fields
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
async gateExplainedForEditorResponse(resolved, capabilities, includeCode = false) {
|
|
1083
|
+
const fieldMap = await getObjectFieldMap(resolved.objectRef);
|
|
1084
|
+
const fields = {};
|
|
1085
|
+
const layers = {};
|
|
1086
|
+
for (const [fieldName, policy] of Object.entries(resolved.fields)) {
|
|
1087
|
+
const fieldDef = fieldMap.get(fieldName);
|
|
1088
|
+
if (!fieldDef || isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
|
|
1089
|
+
fields[fieldName] = policy;
|
|
1090
|
+
layers[fieldName] = (resolved.layers[fieldName] ?? []).filter((layer) => includeCode && layer.layer === "code" || capabilities.manage && (layer.layer === "app" || layer.layer === "tenant") || capabilities.personalize && layer.layer === "user");
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
objectRef: resolved.objectRef,
|
|
1094
|
+
fields,
|
|
1095
|
+
layers
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
toEditorRows(rows, allowedFieldNames) {
|
|
1099
|
+
return Array.from(rows.values()).filter((row) => allowedFieldNames.has(row.fieldName)).map((row) => ({
|
|
1100
|
+
defaultValue: row.defaultValue,
|
|
1101
|
+
displayOrder: row.displayOrder,
|
|
1102
|
+
fieldName: row.fieldName,
|
|
1103
|
+
help: row.help,
|
|
1104
|
+
id: String(row.id),
|
|
1105
|
+
label: row.label,
|
|
1106
|
+
locked: row.locked,
|
|
1107
|
+
scopeType: row.scopeType,
|
|
1108
|
+
tenantId: row.tenantId,
|
|
1109
|
+
updatedBy: row.updatedBy,
|
|
1110
|
+
userId: row.userId,
|
|
1111
|
+
visibility: row.visibility
|
|
1112
|
+
})).sort((left, right) => left.fieldName.localeCompare(right.fieldName));
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
__publicField(FieldPolicyCollection, "_itemClass", FieldPolicy);
|
|
1116
|
+
FieldPolicyCollection = __decorateClass([smrt({
|
|
1117
|
+
conflictColumns: [
|
|
1118
|
+
"object_ref",
|
|
1119
|
+
"field_name",
|
|
1120
|
+
"scope_type",
|
|
1121
|
+
"scope_key"
|
|
1122
|
+
],
|
|
1123
|
+
api: {
|
|
1124
|
+
include: [
|
|
1125
|
+
"create",
|
|
1126
|
+
"update",
|
|
1127
|
+
"delete",
|
|
1128
|
+
"resolveBatch",
|
|
1129
|
+
"getEditorState",
|
|
1130
|
+
"policyAudit"
|
|
1131
|
+
],
|
|
1132
|
+
principalContext: true,
|
|
1133
|
+
routes: {
|
|
1134
|
+
resolveBatch: {
|
|
1135
|
+
scope: "collection",
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
path: "resolve"
|
|
1138
|
+
},
|
|
1139
|
+
getEditorState: {
|
|
1140
|
+
scope: "collection",
|
|
1141
|
+
method: "POST",
|
|
1142
|
+
path: "editor-state"
|
|
1143
|
+
},
|
|
1144
|
+
policyAudit: {
|
|
1145
|
+
scope: "collection",
|
|
1146
|
+
method: "POST",
|
|
1147
|
+
path: "policy-audit"
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
cli: false,
|
|
1152
|
+
mcp: false
|
|
1153
|
+
})], FieldPolicyCollection);
|
|
1154
|
+
function normalizeAuditRefs(value, label, maximum) {
|
|
1155
|
+
if (value === void 0) return [];
|
|
1156
|
+
if (!Array.isArray(value)) throw new Error(`policyAudit ${label} must be a string array`);
|
|
1157
|
+
if (value.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error(`policyAudit ${label} must contain non-empty strings`);
|
|
1158
|
+
const refs = [...new Set(value)];
|
|
1159
|
+
if (refs.length > maximum) throw new Error(`policyAudit ${label} accepts at most ${maximum} objectRefs`);
|
|
1160
|
+
return refs;
|
|
1161
|
+
}
|
|
1162
|
+
function emptyAudit(caller) {
|
|
1163
|
+
return {
|
|
1164
|
+
orgRows: [],
|
|
1165
|
+
appRows: [],
|
|
1166
|
+
inheritedOrgKeys: {},
|
|
1167
|
+
userOverrideCounts: {},
|
|
1168
|
+
driftRows: [],
|
|
1169
|
+
policies: {},
|
|
1170
|
+
caller
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function dateText(value) {
|
|
1174
|
+
if (value instanceof Date) return value.toISOString();
|
|
1175
|
+
return typeof value === "string" ? value : null;
|
|
1176
|
+
}
|
|
1177
|
+
function toAuditRow(row) {
|
|
1178
|
+
return {
|
|
1179
|
+
objectRef: row.objectRef,
|
|
1180
|
+
defaultValue: row.defaultValue,
|
|
1181
|
+
displayOrder: row.displayOrder,
|
|
1182
|
+
fieldName: row.fieldName,
|
|
1183
|
+
help: row.help,
|
|
1184
|
+
id: String(row.id),
|
|
1185
|
+
label: row.label,
|
|
1186
|
+
locked: row.locked,
|
|
1187
|
+
scopeType: row.scopeType,
|
|
1188
|
+
tenantId: row.tenantId,
|
|
1189
|
+
updatedBy: row.updatedBy,
|
|
1190
|
+
userId: row.userId,
|
|
1191
|
+
visibility: row.visibility,
|
|
1192
|
+
createdAt: dateText(row.createdAt),
|
|
1193
|
+
updatedAt: dateText(row.updatedAt)
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function toDriftIdentity(row) {
|
|
1197
|
+
return {
|
|
1198
|
+
id: String(row.id),
|
|
1199
|
+
objectRef: row.objectRef,
|
|
1200
|
+
fieldName: row.fieldName,
|
|
1201
|
+
scopeType: row.scopeType,
|
|
1202
|
+
tenantId: row.tenantId,
|
|
1203
|
+
userId: row.userId,
|
|
1204
|
+
updatedBy: row.updatedBy,
|
|
1205
|
+
createdAt: dateText(row.createdAt),
|
|
1206
|
+
updatedAt: dateText(row.updatedAt)
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function sortAuditRows(left, right) {
|
|
1210
|
+
return left.objectRef.localeCompare(right.objectRef) || left.fieldName.localeCompare(right.fieldName);
|
|
1211
|
+
}
|
|
1212
|
+
function sortDriftRows(left, right) {
|
|
1213
|
+
return left.objectRef.localeCompare(right.objectRef) || left.fieldName.localeCompare(right.fieldName) || left.scopeType.localeCompare(right.scopeType);
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion
|
|
1216
|
+
export { setCachedFieldPolicy as C, ensureFieldPolicyPermissionsRegistered as D, PERSONALIZE_FIELD_POLICY_PERMISSION as E, __exportAll as O, invalidateFieldPolicyCache as S, MANAGE_FIELD_POLICY_PERMISSION as T, requireRegisteredObject as _, buildCodeSeedDelta as a, getCachedFieldPolicy as b, getCodeSeedGroup as c, isPolicyAddressableField as d, isRequiredField as f, isUsableRequiredDefault as g, isTransientField as h, assertDefaultValueMatchesFieldType as i, getFieldReadPermission as l, isStorableReferenceId as m, FieldPolicyCollection_exports as n, buildCodeSeedVisibility as o, isSensitiveField as p, FieldPolicy as r, getCodeDefault as s, FieldPolicyCollection as t, getObjectFieldMap as u, sanitizeFieldUIHints as v, FIELD_POLICY_PERMISSION_DEFINITIONS as w, getFieldPolicyCacheTtlMs as x, clearFieldPolicyCache as y };
|
|
1217
|
+
|
|
1218
|
+
//# sourceMappingURL=FieldPolicyCollection--RxukfCX.js.map
|