@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
package/dist/index.js
CHANGED
|
@@ -1,1211 +1,1202 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as setCachedFieldPolicy, D as ensureFieldPolicyPermissionsRegistered, E as PERSONALIZE_FIELD_POLICY_PERMISSION, O as __exportAll, S as invalidateFieldPolicyCache, T as MANAGE_FIELD_POLICY_PERMISSION, _ as requireRegisteredObject, a as buildCodeSeedDelta, b as getCachedFieldPolicy, c as getCodeSeedGroup, d as isPolicyAddressableField, f as isRequiredField, g as isUsableRequiredDefault, h as isTransientField, i as assertDefaultValueMatchesFieldType, l as getFieldReadPermission, m as isStorableReferenceId, o as buildCodeSeedVisibility, p as isSensitiveField, r as FieldPolicy, s as getCodeDefault, t as FieldPolicyCollection, u as getObjectFieldMap, v as sanitizeFieldUIHints, w as FIELD_POLICY_PERMISSION_DEFINITIONS, x as getFieldPolicyCacheTtlMs, y as clearFieldPolicyCache } from "./chunks/FieldPolicyCollection--RxukfCX.js";
|
|
2
|
+
import { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_SUGGESTION_KINDS, FIELD_POLICY_SUGGESTION_STATUSES, FIELD_POLICY_VISIBILITIES } from "./types.js";
|
|
2
3
|
import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
|
|
3
|
-
import {
|
|
4
|
+
import { assertOperationPermission } from "@happyvertical/smrt-users";
|
|
4
5
|
import { TenantIsolationError, assertTenantReadAllowed, getCurrentTenant, isSuperAdminBypass, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
6
|
import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
|
|
6
|
-
//#region \0rolldown/runtime.js
|
|
7
|
-
var __defProp$2 = Object.defineProperty;
|
|
8
|
-
var __exportAll = (all, no_symbols) => {
|
|
9
|
-
let target = {};
|
|
10
|
-
for (var name in all) __defProp$2(target, name, {
|
|
11
|
-
get: all[name],
|
|
12
|
-
enumerable: true
|
|
13
|
-
});
|
|
14
|
-
if (!no_symbols) __defProp$2(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
-
return target;
|
|
16
|
-
};
|
|
17
|
-
//#endregion
|
|
18
7
|
//#region src/__smrt-register__.ts
|
|
19
|
-
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.61\",\"objects\":{\"@happyvertical/smrt-fields:FieldPolicyCollection\":{\"name\":\"fieldpolicycollection\",\"className\":\"FieldPolicyCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicyCollection\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicyCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"policyAudit\":{\"name\":\"policyAudit\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyAuditSnapshot>\",\"isStatic\":false,\"isPublic\":true},\"getAppRows\":{\"name\":\"getAppRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"getTenantRows\":{\"name\":\"getTenantRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<Map<string, Map<string, FieldPolicy>>>\",\"isStatic\":false,\"isPublic\":true},\"getUserRows\":{\"name\":\"getUserRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"resolveBatch\":{\"name\":\"resolveBatch\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyBatchResult>\",\"isStatic\":false,\"isPublic\":true},\"getEditorState\":{\"name\":\"getEditorState\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyEditorStateResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\",\"resolveBatch\",\"getEditorState\",\"policyAudit\"],\"principalContext\":true,\"routes\":{\"resolveBatch\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"resolve\"},\"getEditorState\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"editor-state\"},\"policyAudit\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"policy-audit\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policies\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicy\",\"exportName\":\"FieldPolicyCollection\",\"collectionExportName\":\"FieldPolicyCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\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);\",\"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\"}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"13aa748c\"}},\"@happyvertical/smrt-fields:FieldPolicy\":{\"name\":\"fieldpolicy\",\"className\":\"FieldPolicy\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicy\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicy.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"scopeType\":{\"type\":\"text\",\"required\":true,\"default\":\"app\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"scopeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"defaultValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"visibility\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"help\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"label\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"displayOrder\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"locked\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{\"nullable\":true}},\"updatedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"getDefaultValue\":{\"name\":\"getDefaultValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"setDefaultValue\":{\"name\":\"setDefaultValue\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"any\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policies\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\"],\"principalContext\":true},\"cli\":{\"include\":[\"create\",\"update\",\"delete\"],\"exclude\":[\"getDefaultValue\",\"setDefaultValue\"]},\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicy\",\"collectionExportName\":\"FieldPolicyCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeType\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\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 \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"scope_type\\\" TEXT NOT NULL DEFAULT 'app',\\n \\\"tenant_id\\\" UUID,\\n \\\"user_id\\\" UUID,\\n \\\"scope_key\\\" TEXT NOT NULL,\\n \\\"default_value\\\" TEXT,\\n \\\"visibility\\\" TEXT,\\n \\\"help\\\" TEXT,\\n \\\"label\\\" TEXT,\\n \\\"display_order\\\" INTEGER,\\n \\\"locked\\\" BOOLEAN,\\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\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"scope_type\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"app\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"scope_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"default_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"visibility\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"help\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"label\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"display_order\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"locked\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"updated_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"34b355cc\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\",\"@happyvertical/smrt-users\"]}"));
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/permissions.ts
|
|
22
|
-
var MANAGE_FIELD_POLICY_PERMISSION = "fields.policy.manage";
|
|
23
|
-
var PERSONALIZE_FIELD_POLICY_PERMISSION = "fields.policy.personalize";
|
|
24
|
-
var FIELD_POLICY_PERMISSION_DEFINITIONS = [{
|
|
25
|
-
slug: MANAGE_FIELD_POLICY_PERMISSION,
|
|
26
|
-
category: "fields",
|
|
27
|
-
name: "Manage Field Policies",
|
|
28
|
-
description: "Manage application and tenant field-policy overrides"
|
|
29
|
-
}, {
|
|
30
|
-
slug: PERSONALIZE_FIELD_POLICY_PERMISSION,
|
|
31
|
-
category: "fields",
|
|
32
|
-
name: "Personalize Field Policies",
|
|
33
|
-
description: "Manage the caller’s own field-policy overrides"
|
|
34
|
-
}];
|
|
35
|
-
var registered = false;
|
|
36
|
-
function ensureFieldPolicyPermissionsRegistered() {
|
|
37
|
-
if (registered) return;
|
|
38
|
-
registered = true;
|
|
39
|
-
registerPermissionDefinitions(FIELD_POLICY_PERMISSION_DEFINITIONS);
|
|
40
|
-
}
|
|
8
|
+
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.63\",\"objects\":{\"@happyvertical/smrt-fields:FieldUsageLearningAgent\":{\"name\":\"fieldusagelearningagent\",\"className\":\"FieldUsageLearningAgent\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageLearningAgent\",\"collection\":\"fieldusagelearningagents\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/usage-learning.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"runUsageMaintenance\":{\"name\":\"runUsageMaintenance\",\"async\":true,\"parameters\":[{\"name\":\"args\",\"type\":\"Record<string>\",\"optional\":true}],\"returnType\":\"Promise<FieldUsageMaintenanceSummary>\",\"isStatic\":false,\"isPublic\":true},\"runSuggestionGeneration\":{\"name\":\"runSuggestionGeneration\",\"async\":true,\"parameters\":[{\"name\":\"args\",\"type\":\"Record<string>\",\"optional\":true}],\"returnType\":\"Promise<FieldUsageSuggestionRunSummary>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_usage_learning_agents\",\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldUsageLearningAgent\",\"collectionExportName\":\"FieldUsageLearningAgentCollection\",\"schema\":{\"tableName\":\"_smrt_field_usage_learning_agents\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_learning_agents\\\" (\\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);\",\"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\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_learning_agents_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_learning_agents_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"183ee1d8\"}},\"@happyvertical/smrt-fields:FieldPolicyCollection\":{\"name\":\"fieldpolicycollection\",\"className\":\"FieldPolicyCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicyCollection\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicyCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"policyAudit\":{\"name\":\"policyAudit\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyAuditSnapshot>\",\"isStatic\":false,\"isPublic\":true},\"getAppRows\":{\"name\":\"getAppRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"getTenantRows\":{\"name\":\"getTenantRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<Map<string, Map<string, FieldPolicy>>>\",\"isStatic\":false,\"isPublic\":true},\"getUserRows\":{\"name\":\"getUserRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"resolveBatch\":{\"name\":\"resolveBatch\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyBatchResult>\",\"isStatic\":false,\"isPublic\":true},\"getEditorState\":{\"name\":\"getEditorState\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyEditorStateResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\",\"resolveBatch\",\"getEditorState\",\"policyAudit\"],\"principalContext\":true,\"routes\":{\"resolveBatch\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"resolve\"},\"getEditorState\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"editor-state\"},\"policyAudit\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"policy-audit\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policies\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicy\",\"exportName\":\"FieldPolicyCollection\",\"collectionExportName\":\"FieldPolicyCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\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);\",\"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\"}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"13aa748c\"}},\"@happyvertical/smrt-fields:FieldPolicySuggestionCollection\":{\"name\":\"fieldpolicysuggestioncollection\",\"className\":\"FieldPolicySuggestionCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicySuggestionCollection\",\"collection\":\"fieldpolicysuggestions\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicySuggestionCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"pendingSuggestions\":{\"name\":\"pendingSuggestions\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<PendingFieldPolicySuggestionsResult>\",\"isStatic\":false,\"isPublic\":true},\"acceptSuggestion\":{\"name\":\"acceptSuggestion\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<AcceptFieldPolicySuggestionResult>\",\"isStatic\":false,\"isPublic\":true},\"dismissSuggestion\":{\"name\":\"dismissSuggestion\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<DismissFieldPolicySuggestionResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"api\":{\"include\":[\"pendingSuggestions\",\"acceptSuggestion\",\"dismissSuggestion\"],\"principalContext\":true,\"routes\":{\"pendingSuggestions\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"pending\"},\"acceptSuggestion\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"accept\"},\"dismissSuggestion\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"dismiss\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policy_suggestions\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicySuggestion\",\"exportName\":\"FieldPolicySuggestionCollection\",\"collectionExportName\":\"FieldPolicySuggestionCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policy_suggestions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policy_suggestions\\\" (\\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);\",\"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\"}},\"indexes\":[{\"name\":\"_smrt_field_policy_suggestions_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policy_suggestions_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"unique\":true}],\"version\":\"853df86d\"}},\"@happyvertical/smrt-fields:FieldUsageCounterCollection\":{\"name\":\"fieldusagecountercollection\",\"className\":\"FieldUsageCounterCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageCounterCollection\",\"collection\":\"fieldusagecounters\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldUsageCounterCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"reportUsage\":{\"name\":\"reportUsage\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldUsageReportResult>\",\"isStatic\":false,\"isPublic\":true},\"listWindow\":{\"name\":\"listWindow\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":false}],\"returnType\":\"Promise<FieldUsageCounter[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"api\":{\"include\":[\"reportUsage\"],\"principalContext\":true,\"routes\":{\"reportUsage\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"report\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_usage_counters\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldUsageCounter\",\"exportName\":\"FieldUsageCounterCollection\",\"collectionExportName\":\"FieldUsageCounterCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_usage_counters\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_counters\\\" (\\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);\",\"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\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_counters_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_counters_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"unique\":true}],\"version\":\"8bd9beae\"}},\"@happyvertical/smrt-fields:FieldUsageReportReceiptCollection\":{\"name\":\"fieldusagereportreceiptcollection\",\"className\":\"FieldUsageReportReceiptCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageReportReceiptCollection\",\"collection\":\"fieldusagereportreceipts\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldUsageCounterCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"claim\":{\"name\":\"claim\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":false}],\"returnType\":\"Promise<boolean>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"api\":false,\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_usage_report_receipts\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldUsageReportReceipt\",\"exportName\":\"FieldUsageReportReceiptCollection\",\"collectionExportName\":\"FieldUsageReportReceiptCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_usage_report_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_report_receipts\\\" (\\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);\",\"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\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_report_receipts_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_report_receipts_tenant_id_user_id_idx\",\"columns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"unique\":true}],\"version\":\"2e77b775\"}},\"@happyvertical/smrt-fields:FieldPolicy\":{\"name\":\"fieldpolicy\",\"className\":\"FieldPolicy\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicy\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicy.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"scopeType\":{\"type\":\"text\",\"required\":true,\"default\":\"app\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"scopeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"defaultValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"visibility\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"help\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"label\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"displayOrder\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"locked\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{\"nullable\":true}},\"updatedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"getDefaultValue\":{\"name\":\"getDefaultValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"setDefaultValue\":{\"name\":\"setDefaultValue\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"any\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policies\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\"],\"principalContext\":true},\"cli\":{\"include\":[\"create\",\"update\",\"delete\"],\"exclude\":[\"getDefaultValue\",\"setDefaultValue\"]},\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicy\",\"collectionExportName\":\"FieldPolicyCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeType\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\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 \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"scope_type\\\" TEXT NOT NULL DEFAULT 'app',\\n \\\"tenant_id\\\" UUID,\\n \\\"user_id\\\" UUID,\\n \\\"scope_key\\\" TEXT NOT NULL,\\n \\\"default_value\\\" TEXT,\\n \\\"visibility\\\" TEXT,\\n \\\"help\\\" TEXT,\\n \\\"label\\\" TEXT,\\n \\\"display_order\\\" INTEGER,\\n \\\"locked\\\" BOOLEAN,\\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\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"scope_type\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"app\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"scope_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"default_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"visibility\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"help\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"label\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"display_order\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"locked\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"updated_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"34b355cc\"}},\"@happyvertical/smrt-fields:FieldPolicySuggestion\":{\"name\":\"fieldpolicysuggestion\",\"className\":\"FieldPolicySuggestion\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicySuggestion\",\"collection\":\"fieldpolicysuggestions\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicySuggestion.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":true,\"autoPopulate\":true,\"nullable\":false}}},\"kind\":{\"type\":\"text\",\"required\":true,\"default\":\"promote\",\"_meta\":{\"required\":true}},\"proposedValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"evidence\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"pending\",\"_meta\":{\"required\":true}},\"activeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"cooldownUntil\":{\"type\":\"datetime\",\"required\":false,\"_meta\":{\"nullable\":true}},\"decidedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"decidedAt\":{\"type\":\"datetime\",\"required\":false,\"_meta\":{\"nullable\":true}}},\"methods\":{\"getEvidence\":{\"name\":\"getEvidence\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setEvidence\":{\"name\":\"setEvidence\",\"async\":false,\"parameters\":[{\"name\":\"evidence\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getProposedValue\":{\"name\":\"getProposedValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"toSuggestionData\":{\"name\":\"toSuggestionData\",\"async\":false,\"parameters\":[],\"returnType\":\"FieldPolicySuggestionData\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policy_suggestions\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicySuggestion\",\"collectionExportName\":\"FieldPolicySuggestionCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"tenantId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"kind\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"activeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policy_suggestions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policy_suggestions\\\" (\\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 \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"tenant_id\\\" UUID NOT NULL,\\n \\\"kind\\\" TEXT NOT NULL DEFAULT 'promote',\\n \\\"proposed_value\\\" TEXT,\\n \\\"evidence\\\" TEXT,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'pending',\\n \\\"active_key\\\" TEXT NOT NULL,\\n \\\"cooldown_until\\\" TIMESTAMP,\\n \\\"decided_by\\\" UUID,\\n \\\"decided_at\\\" TIMESTAMP\\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\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":true,\"unique\":false},\"kind\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"promote\"},\"proposed_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"evidence\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"pending\"},\"active_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"cooldown_until\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"decided_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"decided_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policy_suggestions_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policy_suggestions_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"unique\":true}],\"version\":\"1bc8c6e5\"}},\"@happyvertical/smrt-fields:FieldUsageCounter\":{\"name\":\"fieldusagecounter\",\"className\":\"FieldUsageCounter\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageCounter\",\"collection\":\"fieldusagecounters\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldUsageCounter.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":true,\"autoPopulate\":true,\"nullable\":false}}},\"period\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"submissionCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"setCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"distinctUserCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"distinctUserIds\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"distinctUsersOverflowed\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"valueHistogram\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"valueHistogramOverflowed\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getDistinctUserIds\":{\"name\":\"getDistinctUserIds\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"addDistinctUser\":{\"name\":\"addDistinctUser\",\"async\":false,\"parameters\":[{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getValueHistogram\":{\"name\":\"getValueHistogram\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string, number>\",\"isStatic\":false,\"isPublic\":true},\"recordHistogramSample\":{\"name\":\"recordHistogramSample\",\"async\":false,\"parameters\":[{\"name\":\"key\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"isLegacyBucket\":{\"name\":\"isLegacyBucket\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_usage_counters\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldUsageCounter\",\"collectionExportName\":\"FieldUsageCounterCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"tenantId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"period\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_usage_counters\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_counters\\\" (\\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 \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"tenant_id\\\" UUID NOT NULL,\\n \\\"period\\\" TEXT NOT NULL DEFAULT '',\\n \\\"submission_count\\\" INTEGER,\\n \\\"set_count\\\" INTEGER,\\n \\\"distinct_user_count\\\" INTEGER,\\n \\\"distinct_user_ids\\\" TEXT,\\n \\\"distinct_users_overflowed\\\" BOOLEAN,\\n \\\"value_histogram\\\" TEXT,\\n \\\"value_histogram_overflowed\\\" BOOLEAN\\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\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":true,\"unique\":false},\"period\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"submission_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"set_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"distinct_user_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"distinct_user_ids\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"distinct_users_overflowed\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"value_histogram\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"value_histogram_overflowed\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_usage_counters_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_counters_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"unique\":true}],\"version\":\"46fcebc6\"}},\"@happyvertical/smrt-fields:FieldUsageReportReceipt\":{\"name\":\"fieldusagereportreceipt\",\"className\":\"FieldUsageReportReceipt\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageReportReceipt\",\"collection\":\"fieldusagereportreceipts\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldUsageReportReceipt.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":true,\"autoPopulate\":true,\"nullable\":false}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\"},\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"period\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"_smrt_field_usage_report_receipts\",\"conflictColumns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldUsageReportReceipt\",\"collectionExportName\":\"FieldUsageReportReceiptCollection\",\"validationRules\":[{\"field\":\"tenantId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"period\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_usage_report_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_report_receipts\\\" (\\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 \\\"user_id\\\" UUID,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"period\\\" TEXT NOT NULL DEFAULT ''\\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},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"period\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_report_receipts_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_report_receipts_tenant_id_user_id_idx\",\"columns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"unique\":true}],\"version\":\"c3119047\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\",\"@happyvertical/smrt-users\"]}"));
|
|
41
9
|
//#endregion
|
|
42
|
-
//#region src/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return "db-instance:unknown";
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
return `db-config:${JSON.stringify(dbObject)}`;
|
|
60
|
-
} catch {
|
|
61
|
-
return "db-config:opaque";
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return "db:unknown";
|
|
65
|
-
}
|
|
66
|
-
var hierarchyLoaderIds = /* @__PURE__ */ new WeakMap();
|
|
67
|
-
var nextLoaderId = 1;
|
|
68
|
-
function getHierarchyLoaderNamespace(loader) {
|
|
69
|
-
if (typeof loader !== "function") return "loader:default";
|
|
70
|
-
const key = loader;
|
|
71
|
-
if (!hierarchyLoaderIds.has(key)) hierarchyLoaderIds.set(key, `loader:${nextLoaderId++}`);
|
|
72
|
-
return hierarchyLoaderIds.get(key) ?? "loader:unknown";
|
|
73
|
-
}
|
|
74
|
-
function buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
75
|
-
return `${getDbNamespace(db)}::${objectRef}::${tenantId ?? "app"}::${userId ?? "user:none"}::${getHierarchyLoaderNamespace(hierarchyLoader)}`;
|
|
76
|
-
}
|
|
77
|
-
function getFieldPolicyCacheTtlMs() {
|
|
78
|
-
return FIELD_POLICY_CACHE_TTL_MS;
|
|
79
|
-
}
|
|
80
|
-
function getCachedFieldPolicy(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
81
|
-
const cacheKey = buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader);
|
|
82
|
-
const cached = fieldPolicyCache.get(cacheKey);
|
|
83
|
-
if (!cached) return null;
|
|
84
|
-
if (cached.expiresAt <= Date.now()) {
|
|
85
|
-
fieldPolicyCache.delete(cacheKey);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
return cached.value;
|
|
89
|
-
}
|
|
90
|
-
function setCachedFieldPolicy(objectRef, tenantId, userId, db, value, hierarchyLoader) {
|
|
91
|
-
fieldPolicyCache.set(buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader), {
|
|
92
|
-
expiresAt: Date.now() + FIELD_POLICY_CACHE_TTL_MS,
|
|
93
|
-
value
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function invalidateFieldPolicyCache(objectRef, db) {
|
|
97
|
-
const keyPrefix = `${getDbNamespace(db)}::${objectRef}::`;
|
|
98
|
-
for (const cacheKey of fieldPolicyCache.keys()) if (cacheKey.startsWith(keyPrefix)) fieldPolicyCache.delete(cacheKey);
|
|
99
|
-
}
|
|
100
|
-
function clearFieldPolicyCache() {
|
|
101
|
-
fieldPolicyCache.clear();
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
|
-
//#region src/field-definitions.ts
|
|
105
|
-
function requireRegisteredObject(objectRef) {
|
|
106
|
-
if (!objectRef?.includes(":")) throw new Error(`Field policy objectRef must be a qualified class name ("@package/name:ClassName"), got "${objectRef}"`);
|
|
107
|
-
if (!ObjectRegistry.getClassByQualifiedName(objectRef)) throw new Error(`Unknown field policy objectRef "${objectRef}": no class with that qualified name is registered`);
|
|
108
|
-
}
|
|
109
|
-
async function getObjectFieldMap(objectRef) {
|
|
110
|
-
requireRegisteredObject(objectRef);
|
|
111
|
-
return ObjectRegistry.getAllFields(objectRef);
|
|
112
|
-
}
|
|
113
|
-
function sanitizeFieldUIHints(value) {
|
|
114
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
115
|
-
const raw = value;
|
|
116
|
-
const ui = {
|
|
117
|
-
...typeof raw.basic === "boolean" ? { basic: raw.basic } : {},
|
|
118
|
-
...typeof raw.group === "string" ? { group: raw.group } : {},
|
|
119
|
-
...typeof raw.order === "number" && Number.isFinite(raw.order) ? { order: raw.order } : {},
|
|
120
|
-
...typeof raw.locked === "boolean" ? { locked: raw.locked } : {}
|
|
121
|
-
};
|
|
122
|
-
return Object.keys(ui).length > 0 ? ui : void 0;
|
|
123
|
-
}
|
|
124
|
-
function isSensitiveField(field) {
|
|
125
|
-
return field.sensitive === true || field._meta?.sensitive === true;
|
|
126
|
-
}
|
|
127
|
-
function getFieldReadPermission(field) {
|
|
128
|
-
if (typeof field.readPermission === "string") return field.readPermission;
|
|
129
|
-
const metaPermission = field._meta?.readPermission;
|
|
130
|
-
return typeof metaPermission === "string" ? metaPermission : void 0;
|
|
131
|
-
}
|
|
132
|
-
function isTransientField(field) {
|
|
133
|
-
return field.transient === true || field._meta?.transient === true;
|
|
134
|
-
}
|
|
135
|
-
function isPolicyAddressableField(field) {
|
|
136
|
-
return field._meta?.__smrtSystemField !== true && field.type !== "oneToMany" && field.type !== "manyToMany" && field.type !== "meta";
|
|
137
|
-
}
|
|
138
|
-
function isRequiredField(field) {
|
|
139
|
-
if (field.nullable === true || field._meta?.nullable === true) return false;
|
|
140
|
-
return field.required === true || field._meta?.required === true;
|
|
141
|
-
}
|
|
142
|
-
function getCodeDefault(field) {
|
|
143
|
-
if (field.default !== void 0) return { value: field.default };
|
|
144
|
-
if (field._meta?.default !== void 0) return { value: field._meta.default };
|
|
145
|
-
}
|
|
146
|
-
function isUsableRequiredDefault(defaultBox) {
|
|
147
|
-
if (!defaultBox) return false;
|
|
148
|
-
return defaultBox.value !== null && defaultBox.value !== "";
|
|
149
|
-
}
|
|
150
|
-
function buildCodeSeedVisibility(fields) {
|
|
151
|
-
let hasBasicMarkers = false;
|
|
152
|
-
const hints = /* @__PURE__ */ new Map();
|
|
153
|
-
for (const [name, field] of fields) {
|
|
154
|
-
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
155
|
-
hints.set(name, ui);
|
|
156
|
-
if (ui?.basic === true) hasBasicMarkers = true;
|
|
157
|
-
}
|
|
158
|
-
const visibility = /* @__PURE__ */ new Map();
|
|
159
|
-
for (const [name] of fields) {
|
|
160
|
-
const ui = hints.get(name);
|
|
161
|
-
if (ui?.basic === true) visibility.set(name, "basic");
|
|
162
|
-
else if (ui?.basic === false || hasBasicMarkers) visibility.set(name, "advanced");
|
|
163
|
-
else visibility.set(name, "basic");
|
|
164
|
-
}
|
|
165
|
-
return visibility;
|
|
166
|
-
}
|
|
167
|
-
function buildCodeSeedDelta(field, visibility) {
|
|
168
|
-
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
169
|
-
const help = typeof field.description === "string" ? field.description : typeof field._meta?.description === "string" ? field._meta.description : void 0;
|
|
170
|
-
const codeDefault = getCodeDefault(field);
|
|
171
|
-
return {
|
|
172
|
-
...codeDefault ? { default: codeDefault } : {},
|
|
173
|
-
visibility,
|
|
174
|
-
...help !== void 0 ? { help } : {},
|
|
175
|
-
...ui?.order !== void 0 ? { order: ui.order } : {},
|
|
176
|
-
...ui?.locked === true ? { locked: true } : {}
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
function getCodeSeedGroup(field) {
|
|
180
|
-
return sanitizeFieldUIHints(field._meta?.ui)?.group ?? null;
|
|
181
|
-
}
|
|
182
|
-
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
183
|
-
function usesTextIdStorage(field) {
|
|
184
|
-
return field.idType === "text" || field._meta?.idType === "text";
|
|
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 (!usesTextIdStorage(field) && !UUID_PATTERN.test(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
|
-
}
|
|
10
|
+
//#region src/deterministic-id.ts
|
|
11
|
+
async function deterministicFieldsUuid(parts) {
|
|
12
|
+
const bytes = new TextEncoder().encode(JSON.stringify(parts));
|
|
13
|
+
const uuid = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)).slice(0, 16);
|
|
14
|
+
uuid[6] = uuid[6] & 15 | 80;
|
|
15
|
+
uuid[8] = uuid[8] & 63 | 128;
|
|
16
|
+
const hex = Array.from(uuid, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
17
|
+
return [
|
|
18
|
+
hex.slice(0, 8),
|
|
19
|
+
hex.slice(8, 12),
|
|
20
|
+
hex.slice(12, 16),
|
|
21
|
+
hex.slice(16, 20),
|
|
22
|
+
hex.slice(20)
|
|
23
|
+
].join("-");
|
|
216
24
|
}
|
|
217
25
|
//#endregion
|
|
218
|
-
//#region src/models/
|
|
219
|
-
var __defProp$
|
|
220
|
-
var __getOwnPropDesc$
|
|
221
|
-
var __decorateClass$
|
|
222
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$
|
|
26
|
+
//#region src/models/FieldPolicySuggestion.ts
|
|
27
|
+
var __defProp$5 = Object.defineProperty;
|
|
28
|
+
var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
|
|
29
|
+
var __decorateClass$5 = (decorators, target, key, kind) => {
|
|
30
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
|
|
223
31
|
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$
|
|
32
|
+
if (kind && result) __defProp$5(target, key, result);
|
|
225
33
|
return result;
|
|
226
34
|
};
|
|
227
|
-
|
|
228
|
-
var
|
|
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 {
|
|
35
|
+
var ACTIVE_SUGGESTION_KEY = "active";
|
|
36
|
+
var FieldPolicySuggestion = class extends SmrtObject {
|
|
277
37
|
objectRef = "";
|
|
278
38
|
fieldName = "";
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
locked = null;
|
|
289
|
-
updatedBy = null;
|
|
39
|
+
tenantId;
|
|
40
|
+
kind = "promote";
|
|
41
|
+
proposedValue = null;
|
|
42
|
+
evidence = "{}";
|
|
43
|
+
status = "pending";
|
|
44
|
+
activeKey = ACTIVE_SUGGESTION_KEY;
|
|
45
|
+
cooldownUntil = null;
|
|
46
|
+
decidedBy = null;
|
|
47
|
+
decidedAt = null;
|
|
290
48
|
constructor(options = {}) {
|
|
291
49
|
super(options);
|
|
292
50
|
if (options.objectRef !== void 0) this.objectRef = options.objectRef;
|
|
293
51
|
if (options.fieldName !== void 0) this.fieldName = options.fieldName;
|
|
294
|
-
if (options.scopeType !== void 0) this.scopeType = options.scopeType;
|
|
295
52
|
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
296
|
-
if (options.
|
|
297
|
-
if (options.
|
|
298
|
-
if (options.
|
|
299
|
-
|
|
300
|
-
if (options.
|
|
301
|
-
if (options.
|
|
302
|
-
if (options.
|
|
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;
|
|
53
|
+
if (options.kind !== void 0) this.kind = options.kind;
|
|
54
|
+
if (options.proposedValue !== void 0) this.proposedValue = options.proposedValue;
|
|
55
|
+
if (options.evidence !== void 0) this.evidence = options.evidence;
|
|
56
|
+
if (options.status !== void 0) this.status = options.status;
|
|
57
|
+
if (options.cooldownUntil !== void 0) this.cooldownUntil = options.cooldownUntil;
|
|
58
|
+
if (options.decidedBy !== void 0) this.decidedBy = options.decidedBy;
|
|
59
|
+
if (options.decidedAt !== void 0) this.decidedAt = options.decidedAt;
|
|
306
60
|
}
|
|
307
|
-
/** Parse the stored
|
|
308
|
-
|
|
309
|
-
if (this.defaultValue === null || this.defaultValue === void 0) return;
|
|
61
|
+
/** Parse the stored evidence object (guarded; junk parses as empty). */
|
|
62
|
+
getEvidence() {
|
|
310
63
|
try {
|
|
311
|
-
|
|
64
|
+
const parsed = JSON.parse(this.evidence);
|
|
65
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
312
66
|
} catch {
|
|
313
|
-
return;
|
|
67
|
+
return {};
|
|
314
68
|
}
|
|
315
69
|
}
|
|
316
|
-
/** Serialize
|
|
317
|
-
|
|
318
|
-
this.
|
|
70
|
+
/** Serialize an evidence object into the stored JSON string. */
|
|
71
|
+
setEvidence(evidence) {
|
|
72
|
+
this.evidence = JSON.stringify(evidence);
|
|
319
73
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.
|
|
325
|
-
|
|
74
|
+
/** Parse the proposed value; `undefined` when none is stored. */
|
|
75
|
+
getProposedValue() {
|
|
76
|
+
if (this.proposedValue === null || this.proposedValue === void 0) return;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(this.proposedValue);
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
326
81
|
}
|
|
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
82
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return
|
|
83
|
+
/** Serialized row shape for the collection actions. */
|
|
84
|
+
toSuggestionData() {
|
|
85
|
+
return {
|
|
86
|
+
id: String(this.id),
|
|
87
|
+
objectRef: this.objectRef,
|
|
88
|
+
fieldName: this.fieldName,
|
|
89
|
+
tenantId: String(this.tenantId ?? ""),
|
|
90
|
+
kind: this.kind,
|
|
91
|
+
proposedValue: this.proposedValue ?? null,
|
|
92
|
+
evidence: this.getEvidence(),
|
|
93
|
+
status: this.status,
|
|
94
|
+
cooldownUntil: toIsoOrNull(this.cooldownUntil),
|
|
95
|
+
decidedBy: this.decidedBy ?? null,
|
|
96
|
+
decidedAt: toIsoOrNull(this.decidedAt)
|
|
97
|
+
};
|
|
343
98
|
}
|
|
344
|
-
async
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
}
|
|
99
|
+
async save() {
|
|
100
|
+
await this.assertRowOwnedByAmbientContext("save");
|
|
101
|
+
await this.validateFieldPolicySuggestion();
|
|
102
|
+
this.applyActiveKey();
|
|
103
|
+
return super.save();
|
|
365
104
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return
|
|
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;
|
|
105
|
+
/**
|
|
106
|
+
* Recompute the lifecycle-slot key: the shared sentinel while pending (so
|
|
107
|
+
* the unique index admits exactly one), the row's own id once settled (so
|
|
108
|
+
* history never competes for the slot and the freed slot allows a
|
|
109
|
+
* post-cool-down regeneration). A settled row that has not been persisted
|
|
110
|
+
* yet is assigned its id here — the key must be unique from the first write.
|
|
111
|
+
*/
|
|
112
|
+
applyActiveKey() {
|
|
113
|
+
if (this.status === "pending") {
|
|
114
|
+
this.activeKey = ACTIVE_SUGGESTION_KEY;
|
|
115
|
+
return;
|
|
383
116
|
}
|
|
117
|
+
if (!this.id) this.id = crypto.randomUUID();
|
|
118
|
+
this.activeKey = String(this.id);
|
|
384
119
|
}
|
|
385
120
|
async delete() {
|
|
386
|
-
|
|
387
|
-
if (persisted) this.assertScopeOwnedByAmbientContext(persisted, "delete");
|
|
388
|
-
await this.assertScopePermission(persisted ?? this);
|
|
389
|
-
const objectRef = persisted?.objectRef ?? this.objectRef;
|
|
121
|
+
await this.assertRowOwnedByAmbientContext("delete");
|
|
390
122
|
await super.delete();
|
|
391
|
-
invalidateFieldPolicyCache(objectRef, this.db);
|
|
392
|
-
if (this.objectRef && this.objectRef !== objectRef) invalidateFieldPolicyCache(this.objectRef, this.db);
|
|
393
123
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
124
|
+
/**
|
|
125
|
+
* The "normal validation" the #1885 seam promises producers: registry-known
|
|
126
|
+
* field, policy-addressable, never sensitive/read-permission-gated/transient
|
|
127
|
+
* (those fields are count-only in usage data and get no suggestions), valid
|
|
128
|
+
* kind/status, and for `default` suggestions a JSON proposed value that
|
|
129
|
+
* type-checks against the manifest field type.
|
|
130
|
+
*/
|
|
131
|
+
async validateFieldPolicySuggestion() {
|
|
132
|
+
if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldPolicySuggestion.objectRef is required");
|
|
133
|
+
if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldPolicySuggestion.fieldName is required");
|
|
134
|
+
if (!this.tenantId) throw new Error("FieldPolicySuggestion.tenantId is required");
|
|
135
|
+
if (!FIELD_POLICY_SUGGESTION_KINDS.includes(this.kind)) throw new Error(`FieldPolicySuggestion.kind must be one of ${FIELD_POLICY_SUGGESTION_KINDS.join(", ")}; got "${this.kind}"`);
|
|
136
|
+
if (!FIELD_POLICY_SUGGESTION_STATUSES.includes(this.status)) throw new Error(`FieldPolicySuggestion.status must be one of ${FIELD_POLICY_SUGGESTION_STATUSES.join(", ")}; got "${this.status}"`);
|
|
402
137
|
const fieldDef = (await getObjectFieldMap(this.objectRef)).get(this.fieldName);
|
|
403
138
|
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
|
|
405
|
-
if (fieldDef
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
139
|
+
if (fieldDef._meta?.__smrtSystemField === true || fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany" || fieldDef.type === "meta") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is not policy-addressable, so it cannot carry a suggestion`);
|
|
140
|
+
if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is sensitive, read-permission-gated, or transient; usage data for it is count-only and it cannot carry a suggestion`);
|
|
141
|
+
if (this.kind === "default") {
|
|
142
|
+
if (this.proposedValue === null) throw new Error("FieldPolicySuggestion of kind 'default' requires a proposedValue");
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(this.proposedValue);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new Error(`FieldPolicySuggestion.proposedValue is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
411
149
|
assertDefaultValueMatchesFieldType(this.objectRef, this.fieldName, fieldDef, parsed);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
150
|
+
} else if (this.proposedValue !== null) throw new Error("FieldPolicySuggestion of kind 'promote' must not carry a proposedValue");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Tenant write boundary (the FieldPolicy posture): inside a non-bypass
|
|
154
|
+
* tenant context a caller may only touch its own tenant's suggestions —
|
|
155
|
+
* checked against BOTH the in-memory scope and, for persisted rows, the
|
|
156
|
+
* PERSISTED tenant (a foreign row cannot be re-scoped into the caller's
|
|
157
|
+
* tenant). Trusted execution (no context / bypass) is exempt — that is what
|
|
158
|
+
* lets the scheduled generation job and platform flows operate.
|
|
159
|
+
*/
|
|
160
|
+
async assertRowOwnedByAmbientContext(operation) {
|
|
161
|
+
const context = getCurrentTenant();
|
|
162
|
+
if (!context || isSuperAdminBypass()) return;
|
|
163
|
+
if (this.tenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldPolicySuggestion.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${this.tenantId}'`, {
|
|
164
|
+
tenantId: context.tenantId,
|
|
165
|
+
attemptedTenantId: this.tenantId ?? void 0
|
|
166
|
+
});
|
|
167
|
+
if (this.id) {
|
|
168
|
+
const persisted = await this.db.get(this.tableName, { id: this.id });
|
|
169
|
+
if (persisted) {
|
|
170
|
+
const row = persisted;
|
|
171
|
+
const persistedTenant = row.tenantId ?? row.tenant_id ?? this.tenantId ?? null;
|
|
172
|
+
if (persistedTenant !== null && String(persistedTenant) !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldPolicySuggestion.${operation}: the persisted row belongs to '${String(persistedTenant)}'`, {
|
|
173
|
+
tenantId: context.tenantId,
|
|
174
|
+
attemptedTenantId: String(persistedTenant)
|
|
175
|
+
});
|
|
421
176
|
}
|
|
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
177
|
}
|
|
424
178
|
}
|
|
179
|
+
};
|
|
180
|
+
__decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "objectRef", 2);
|
|
181
|
+
__decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "fieldName", 2);
|
|
182
|
+
__decorateClass$5([tenantId()], FieldPolicySuggestion.prototype, "tenantId", 2);
|
|
183
|
+
__decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "kind", 2);
|
|
184
|
+
__decorateClass$5([field({
|
|
185
|
+
type: "text",
|
|
186
|
+
nullable: true
|
|
187
|
+
})], FieldPolicySuggestion.prototype, "proposedValue", 2);
|
|
188
|
+
__decorateClass$5([field({ type: "text" })], FieldPolicySuggestion.prototype, "evidence", 2);
|
|
189
|
+
__decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "status", 2);
|
|
190
|
+
__decorateClass$5([field({
|
|
191
|
+
type: "text",
|
|
192
|
+
required: true
|
|
193
|
+
})], FieldPolicySuggestion.prototype, "activeKey", 2);
|
|
194
|
+
__decorateClass$5([field({
|
|
195
|
+
type: "datetime",
|
|
196
|
+
nullable: true
|
|
197
|
+
})], FieldPolicySuggestion.prototype, "cooldownUntil", 2);
|
|
198
|
+
__decorateClass$5([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicySuggestion.prototype, "decidedBy", 2);
|
|
199
|
+
__decorateClass$5([field({
|
|
200
|
+
type: "datetime",
|
|
201
|
+
nullable: true
|
|
202
|
+
})], FieldPolicySuggestion.prototype, "decidedAt", 2);
|
|
203
|
+
FieldPolicySuggestion = __decorateClass$5([smrt({
|
|
204
|
+
tableName: "_smrt_field_policy_suggestions",
|
|
205
|
+
conflictColumns: [
|
|
206
|
+
"object_ref",
|
|
207
|
+
"field_name",
|
|
208
|
+
"tenant_id",
|
|
209
|
+
"kind",
|
|
210
|
+
"active_key"
|
|
211
|
+
],
|
|
212
|
+
api: { include: [] },
|
|
213
|
+
cli: false,
|
|
214
|
+
mcp: { include: [] }
|
|
215
|
+
})], FieldPolicySuggestion);
|
|
216
|
+
function toIsoOrNull(value) {
|
|
217
|
+
if (value === null || value === void 0) return null;
|
|
218
|
+
if (value instanceof Date) return value.toISOString();
|
|
219
|
+
const parsed = Date.parse(value);
|
|
220
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/collections/FieldPolicySuggestionCollection.ts
|
|
224
|
+
var __defProp$4 = Object.defineProperty;
|
|
225
|
+
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
|
|
226
|
+
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
|
|
227
|
+
enumerable: true,
|
|
228
|
+
configurable: true,
|
|
229
|
+
writable: true,
|
|
230
|
+
value
|
|
231
|
+
}) : obj[key] = value;
|
|
232
|
+
var __decorateClass$4 = (decorators, target, key, kind) => {
|
|
233
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
|
|
234
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
235
|
+
if (kind && result) __defProp$4(target, key, result);
|
|
236
|
+
return result;
|
|
237
|
+
};
|
|
238
|
+
var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
239
|
+
var FieldPolicySuggestionConflictError = class extends Error {
|
|
240
|
+
/** Core's generated-REST status contract (own integer property). */
|
|
241
|
+
httpStatus = 409;
|
|
242
|
+
/** The users-style shape mirrored by this package's authorization errors. */
|
|
243
|
+
status = 409;
|
|
244
|
+
constructor(operation, suggestionId) {
|
|
245
|
+
super(`${operation}: suggestion "${suggestionId}" was already decided by another request (it is no longer pending)`);
|
|
246
|
+
this.name = "FieldPolicySuggestionConflictError";
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var FieldPolicySuggestionRequestContextError = class extends TenantIsolationError {
|
|
250
|
+
httpStatus = 403;
|
|
251
|
+
status = 403;
|
|
252
|
+
constructor(message, details) {
|
|
253
|
+
super(message, details);
|
|
254
|
+
this.name = "FieldPolicySuggestionRequestContextError";
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
function transactionBoundOptions(db) {
|
|
258
|
+
return {
|
|
259
|
+
db,
|
|
260
|
+
_reuseInitializedDb: true,
|
|
261
|
+
_deferRuntimeInitialization: true
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function claimSuggestionTransition(db, suggestionId, patch) {
|
|
265
|
+
return ((await db.query(`UPDATE _smrt_field_policy_suggestions
|
|
266
|
+
SET status = ?,
|
|
267
|
+
active_key = ?,
|
|
268
|
+
decided_at = ?,
|
|
269
|
+
decided_by = ?,
|
|
270
|
+
cooldown_until = ?
|
|
271
|
+
WHERE id = ? AND status = 'pending'
|
|
272
|
+
RETURNING id`, patch.status, patch.activeKey, patch.decidedAt.toISOString(), patch.decidedBy, patch.cooldownUntil ? patch.cooldownUntil.toISOString() : null, suggestionId))?.rows?.length ?? 0) > 0;
|
|
273
|
+
}
|
|
274
|
+
async function revertSuggestionToPending(db, suggestionId) {
|
|
275
|
+
await db.query(`UPDATE _smrt_field_policy_suggestions
|
|
276
|
+
SET status = 'pending',
|
|
277
|
+
active_key = ?,
|
|
278
|
+
decided_at = NULL,
|
|
279
|
+
decided_by = NULL,
|
|
280
|
+
cooldown_until = NULL
|
|
281
|
+
WHERE id = ?`, ACTIVE_SUGGESTION_KEY, suggestionId);
|
|
282
|
+
}
|
|
283
|
+
async function settleSuggestionActiveKey(db, suggestionId) {
|
|
284
|
+
await db.query(`UPDATE _smrt_field_policy_suggestions
|
|
285
|
+
SET active_key = ?
|
|
286
|
+
WHERE id = ?`, suggestionId, suggestionId);
|
|
287
|
+
}
|
|
288
|
+
var FieldPolicySuggestionCompensationError = class extends Error {
|
|
289
|
+
httpStatus = 500;
|
|
290
|
+
status = 500;
|
|
291
|
+
revertCause;
|
|
292
|
+
constructor(suggestionId, cause, revertCause) {
|
|
293
|
+
super(`acceptSuggestion: the policy write for suggestion "${suggestionId}" failed AND reverting it to pending also failed; the suggestion may be left accepted without its policy row and needs reconciliation (policy error: ${cause instanceof Error ? cause.message : String(cause)}; revert error: ${revertCause instanceof Error ? revertCause.message : String(revertCause)})`, { cause });
|
|
294
|
+
this.name = "FieldPolicySuggestionCompensationError";
|
|
295
|
+
this.revertCause = revertCause;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function fieldPolicyRowId(tenantId, objectRef, fieldName) {
|
|
299
|
+
return deterministicFieldsUuid([
|
|
300
|
+
"field-policy-row",
|
|
301
|
+
"tenant",
|
|
302
|
+
tenantId,
|
|
303
|
+
objectRef,
|
|
304
|
+
fieldName
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
async function updatePolicyColumn(db, target) {
|
|
308
|
+
const id = ((await db.query(`UPDATE _smrt_field_policies
|
|
309
|
+
SET ${target.column} = ?,
|
|
310
|
+
updated_by = ?,
|
|
311
|
+
updated_at = ?
|
|
312
|
+
WHERE object_ref = ?
|
|
313
|
+
AND field_name = ?
|
|
314
|
+
AND scope_type = 'tenant'
|
|
315
|
+
AND tenant_id = ?
|
|
316
|
+
RETURNING id`, target.value, target.decidedBy, (/* @__PURE__ */ new Date()).toISOString(), target.objectRef, target.fieldName, target.tenantId))?.rows ?? [])[0]?.id;
|
|
317
|
+
return id === void 0 || id === null ? null : String(id);
|
|
318
|
+
}
|
|
319
|
+
async function assertAcceptedDefaultStillAllowed(suggestion) {
|
|
320
|
+
const fieldDef = await assertAcceptedPolicyTargetStillAllowed(suggestion);
|
|
321
|
+
if (suggestion.proposedValue === null) throw new Error("FieldPolicySuggestion of kind 'default' requires a proposedValue");
|
|
322
|
+
let parsed;
|
|
323
|
+
try {
|
|
324
|
+
parsed = JSON.parse(suggestion.proposedValue);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw new Error(`FieldPolicySuggestion.proposedValue is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
+
}
|
|
328
|
+
assertDefaultValueMatchesFieldType(suggestion.objectRef, suggestion.fieldName, fieldDef, parsed);
|
|
329
|
+
}
|
|
330
|
+
async function assertAcceptedPolicyTargetStillAllowed(suggestion) {
|
|
331
|
+
const fieldDef = (await getObjectFieldMap(suggestion.objectRef)).get(suggestion.fieldName);
|
|
332
|
+
if (!fieldDef || !isPolicyAddressableField(fieldDef)) throw new Error(`Field "${suggestion.fieldName}" on "${suggestion.objectRef}" is not policy-addressable`);
|
|
333
|
+
if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) throw new Error(`Cannot apply a policy for "${suggestion.objectRef}.${suggestion.fieldName}": the field is sensitive, read-permission-gated, or transient`);
|
|
334
|
+
return fieldDef;
|
|
335
|
+
}
|
|
336
|
+
var DEFAULT_SUGGESTION_COOL_DOWN_MS = 720 * 60 * 60 * 1e3;
|
|
337
|
+
var MIN_SUGGESTION_COOL_DOWN_MS = 3600 * 1e3;
|
|
338
|
+
var MAX_SUGGESTION_COOL_DOWN_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
339
|
+
var MAX_PENDING_OBJECT_REFS = 100;
|
|
340
|
+
var FieldPolicySuggestionCollection = class extends SmrtCollection {
|
|
425
341
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
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.
|
|
342
|
+
* The caller's tenant's PENDING suggestions (optionally filtered to a set
|
|
343
|
+
* of objectRefs), newest first, plus the total for the gear badge.
|
|
437
344
|
*/
|
|
438
|
-
async
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
tenantId
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
345
|
+
async pendingSuggestions(options = {}) {
|
|
346
|
+
const tenantId = await this.requireManageContext("pendingSuggestions");
|
|
347
|
+
const objectRefs = normalizeObjectRefsFilter(options.objectRefs);
|
|
348
|
+
const where = {
|
|
349
|
+
tenantId,
|
|
350
|
+
status: "pending"
|
|
351
|
+
};
|
|
352
|
+
if (objectRefs) where["objectRef in"] = objectRefs;
|
|
353
|
+
const rows = await this.list({
|
|
354
|
+
where,
|
|
355
|
+
orderBy: "created_at DESC"
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
suggestions: rows.map((row) => row.toSuggestionData()),
|
|
359
|
+
total: rows.length
|
|
360
|
+
};
|
|
446
361
|
}
|
|
447
362
|
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
363
|
+
* Accept a pending suggestion: CLAIM the pending→accepted transition, then
|
|
364
|
+
* write the corresponding org-scope `FieldPolicy` row THROUGH NORMAL
|
|
365
|
+
* VALIDATION (registry check, type check, security rail, required-field
|
|
366
|
+
* invariant, ownership boundary, and the #2049 permission split — the
|
|
367
|
+
* ambient caller must hold `fields.policy.manage`, which this action also
|
|
368
|
+
* asserts up front).
|
|
369
|
+
*
|
|
370
|
+
* An existing tenant row for the same `(objectRef, fieldName)` is UPDATED
|
|
371
|
+
* (read-modify-write), preserving its other sparse columns — never
|
|
372
|
+
* duplicated through the natural-key upsert.
|
|
373
|
+
*
|
|
374
|
+
* Concurrency: the claim is a compare-and-set on `status` (the jobs
|
|
375
|
+
* `writeOwnedJob` precedent — a guarded UPDATE with `RETURNING id`), so an
|
|
376
|
+
* overlapping accept/dismiss pair cannot both win; the loser throws
|
|
377
|
+
* {@link FieldPolicySuggestionConflictError} (409). Claim and policy write
|
|
378
|
+
* run in ONE transaction when the driver supports it, so a rejected policy
|
|
379
|
+
* write can never leave the suggestion settled without its policy (and vice
|
|
380
|
+
* versa). Drivers without transactions get an explicit compensating revert.
|
|
451
381
|
*/
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
382
|
+
async acceptSuggestion(options = {}) {
|
|
383
|
+
const tenantId = await this.requireManageContext("acceptSuggestion");
|
|
384
|
+
const suggestion = await this.loadOwnedPendingSuggestion("acceptSuggestion", options.id, tenantId);
|
|
385
|
+
const suggestionId = String(suggestion.id);
|
|
386
|
+
const decidedBy = getCurrentTenant()?.userId ?? null;
|
|
387
|
+
const decidedAt = /* @__PURE__ */ new Date();
|
|
388
|
+
const claim = {
|
|
389
|
+
status: "accepted",
|
|
390
|
+
activeKey: suggestionId,
|
|
391
|
+
decidedAt,
|
|
392
|
+
decidedBy,
|
|
393
|
+
cooldownUntil: null
|
|
394
|
+
};
|
|
395
|
+
const tx = await this.beginTransactionIfSupported();
|
|
396
|
+
if (tx) {
|
|
397
|
+
let policyRowId2;
|
|
398
|
+
try {
|
|
399
|
+
if (!await claimSuggestionTransition(tx, suggestionId, claim)) throw new FieldPolicySuggestionConflictError("acceptSuggestion", suggestionId);
|
|
400
|
+
policyRowId2 = await this.applyAcceptedPolicy(tx, suggestion, tenantId, decidedBy);
|
|
401
|
+
await tx.commit();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
try {
|
|
404
|
+
await tx.rollback();
|
|
405
|
+
} catch {}
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
invalidateFieldPolicyCache(suggestion.objectRef, this.db);
|
|
409
|
+
suggestion.status = "accepted";
|
|
410
|
+
suggestion.decidedAt = decidedAt;
|
|
411
|
+
suggestion.decidedBy = decidedBy;
|
|
412
|
+
return {
|
|
413
|
+
suggestion: suggestion.toSuggestionData(),
|
|
414
|
+
policyRowId: policyRowId2
|
|
415
|
+
};
|
|
456
416
|
}
|
|
457
|
-
if (this.
|
|
458
|
-
|
|
459
|
-
|
|
417
|
+
if (!await claimSuggestionTransition(this.db, suggestionId, {
|
|
418
|
+
...claim,
|
|
419
|
+
activeKey: "active"
|
|
420
|
+
})) throw new FieldPolicySuggestionConflictError("acceptSuggestion", suggestionId);
|
|
421
|
+
let policyRowId;
|
|
422
|
+
try {
|
|
423
|
+
policyRowId = await this.applyAcceptedPolicy(this.db, suggestion, tenantId, decidedBy);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
try {
|
|
426
|
+
await revertSuggestionToPending(this.db, suggestionId);
|
|
427
|
+
} catch (revertError) {
|
|
428
|
+
throw new FieldPolicySuggestionCompensationError(suggestionId, error, revertError);
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
460
431
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
432
|
+
await settleSuggestionActiveKey(this.db, suggestionId);
|
|
433
|
+
invalidateFieldPolicyCache(suggestion.objectRef, this.db);
|
|
434
|
+
suggestion.status = "accepted";
|
|
435
|
+
suggestion.decidedAt = decidedAt;
|
|
436
|
+
suggestion.decidedBy = decidedBy;
|
|
437
|
+
return {
|
|
438
|
+
suggestion: suggestion.toSuggestionData(),
|
|
439
|
+
policyRowId
|
|
440
|
+
};
|
|
466
441
|
}
|
|
467
442
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
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.
|
|
443
|
+
* Dismiss a pending suggestion: set status + a cool-down until which the
|
|
444
|
+
* generation job will NOT regenerate the same
|
|
445
|
+
* `(objectRef, fieldName, tenantId, kind)` suggestion.
|
|
477
446
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
* `tenantId` through a server-side model call, because the generated routes
|
|
487
|
-
* still strip it.
|
|
447
|
+
* Validates OWNERSHIP and PENDING STATUS ONLY — deliberately NOT continued
|
|
448
|
+
* proposal eligibility. A queued suggestion whose field has since been
|
|
449
|
+
* removed, turned sensitive/`readPermission`-gated/transient, or retyped is
|
|
450
|
+
* exactly the row an operator most needs to clear; re-running proposal
|
|
451
|
+
* validation on the way out would reject the dismissal and, because pending
|
|
452
|
+
* rows are never pruned, wedge it in the queue forever. The claim is the same
|
|
453
|
+
* compare-and-set as accept, so it also bypasses the model's proposal
|
|
454
|
+
* validation by construction.
|
|
488
455
|
*/
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
456
|
+
async dismissSuggestion(options = {}) {
|
|
457
|
+
const tenantId = await this.requireManageContext("dismissSuggestion");
|
|
458
|
+
const suggestion = await this.loadOwnedPendingSuggestion("dismissSuggestion", options.id, tenantId);
|
|
459
|
+
const suggestionId = String(suggestion.id);
|
|
460
|
+
const coolDownMs = normalizeCoolDownMs(options.coolDownMs);
|
|
461
|
+
const decidedAt = /* @__PURE__ */ new Date();
|
|
462
|
+
const decidedBy = getCurrentTenant()?.userId ?? null;
|
|
463
|
+
const cooldownUntil = new Date(decidedAt.getTime() + coolDownMs);
|
|
464
|
+
if (!await claimSuggestionTransition(this.db, suggestionId, {
|
|
465
|
+
status: "dismissed",
|
|
466
|
+
activeKey: suggestionId,
|
|
467
|
+
decidedAt,
|
|
468
|
+
decidedBy,
|
|
469
|
+
cooldownUntil
|
|
470
|
+
})) throw new FieldPolicySuggestionConflictError("dismissSuggestion", suggestionId);
|
|
471
|
+
suggestion.status = "dismissed";
|
|
472
|
+
suggestion.cooldownUntil = cooldownUntil;
|
|
473
|
+
suggestion.decidedAt = decidedAt;
|
|
474
|
+
suggestion.decidedBy = decidedBy;
|
|
475
|
+
return { suggestion: suggestion.toSuggestionData() };
|
|
494
476
|
}
|
|
495
477
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* existing row.
|
|
478
|
+
* Write the org-scope policy column an accepted suggestion implies, bound to
|
|
479
|
+
* `db` so it participates in the caller's transaction.
|
|
499
480
|
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
481
|
+
* A `promote` and a `default` suggestion for the SAME field share one sparse
|
|
482
|
+
* `FieldPolicy` row but own DIFFERENT columns, and both may be accepted
|
|
483
|
+
* concurrently. A read-modify-save of the whole row therefore loses updates:
|
|
484
|
+
* each acceptance reads the row (or its absence) before the other's save
|
|
485
|
+
* lands, and the later full-row write erases the sibling's column — and, via
|
|
486
|
+
* the natural-key upsert, can even replace the row id, dangling the earlier
|
|
487
|
+
* caller's `policyRowId`. (Distinct from the suggestion-row race fixed
|
|
488
|
+
* earlier: that one guards the pending→settled transition; this one is on the
|
|
489
|
+
* shared policy row.) So this NEVER writes a whole row over an existing one:
|
|
509
490
|
*
|
|
510
|
-
*
|
|
491
|
+
* 1. An ATOMIC partial UPDATE sets only this acceptance's column, keyed by
|
|
492
|
+
* the policy natural key and `RETURNING id` — one statement, so no
|
|
493
|
+
* interleaving can drop the sibling's column, and the returned id is
|
|
494
|
+
* always the surviving row.
|
|
495
|
+
* 2. Only when no row exists yet does it CREATE through the model, so full
|
|
496
|
+
* validation (registry, type check, security rail, required-field
|
|
497
|
+
* invariant, ownership + permission split) runs on the row that is
|
|
498
|
+
* actually inserted. The row carries a DETERMINISTIC id under strict
|
|
499
|
+
* insert, so two concurrent creates cannot each mint a row: the loser
|
|
500
|
+
* collides and falls back to step 1, applying only its own column.
|
|
501
|
+
*
|
|
502
|
+
* The update path skips the model, so BOTH kinds re-check live target
|
|
503
|
+
* addressability through the shared helpers (a field can turn sensitive,
|
|
504
|
+
* gated, transient, or disappear between queueing and acceptance). Defaults
|
|
505
|
+
* additionally re-check their value/type rail; a promotion to `basic` has no
|
|
506
|
+
* value payload, and the required-field invariant restricts only
|
|
507
|
+
* advanced/hidden. Org locks constrain the user tier, not org rows.
|
|
511
508
|
*/
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
509
|
+
async applyAcceptedPolicy(db, suggestion, tenantId, decidedBy) {
|
|
510
|
+
if (suggestion.kind === "default") await assertAcceptedDefaultStillAllowed(suggestion);
|
|
511
|
+
else await assertAcceptedPolicyTargetStillAllowed(suggestion);
|
|
512
|
+
const target = {
|
|
513
|
+
objectRef: suggestion.objectRef,
|
|
514
|
+
fieldName: suggestion.fieldName,
|
|
515
|
+
tenantId,
|
|
516
|
+
column: suggestion.kind === "promote" ? "visibility" : "default_value",
|
|
517
|
+
value: suggestion.kind === "promote" ? "basic" : suggestion.proposedValue,
|
|
518
|
+
decidedBy
|
|
519
|
+
};
|
|
520
|
+
const updatedId = await updatePolicyColumn(db, target);
|
|
521
|
+
if (updatedId) return updatedId;
|
|
522
|
+
const { FieldPolicyCollection } = await import("./chunks/FieldPolicyCollection--RxukfCX.js").then((n) => n.n);
|
|
523
|
+
const policies = await FieldPolicyCollection.create(transactionBoundOptions(db));
|
|
524
|
+
const deterministicId = await fieldPolicyRowId(tenantId, suggestion.objectRef, suggestion.fieldName);
|
|
525
|
+
try {
|
|
526
|
+
const created = await policies.create({
|
|
527
|
+
id: deterministicId,
|
|
528
|
+
objectRef: suggestion.objectRef,
|
|
529
|
+
fieldName: suggestion.fieldName,
|
|
530
|
+
scopeType: "tenant",
|
|
531
|
+
tenantId,
|
|
532
|
+
...suggestion.kind === "promote" ? { visibility: "basic" } : { defaultValue: suggestion.proposedValue },
|
|
533
|
+
updatedBy: decidedBy,
|
|
534
|
+
_insertOnly: true
|
|
535
|
+
});
|
|
536
|
+
return String(created.id);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
const racedId = await updatePolicyColumn(db, target);
|
|
539
|
+
if (racedId) return racedId;
|
|
540
|
+
throw error;
|
|
527
541
|
}
|
|
528
542
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
543
|
+
/**
|
|
544
|
+
* A driver transaction handle, or `null` when the driver has none (the
|
|
545
|
+
* `FieldPolicy.saveAfterIdentityChange` probe, same shape).
|
|
546
|
+
*/
|
|
547
|
+
async beginTransactionIfSupported() {
|
|
548
|
+
if (typeof this.db.beginTransaction !== "function") return null;
|
|
549
|
+
return await this.db.beginTransaction() ?? null;
|
|
535
550
|
}
|
|
536
551
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
552
|
+
* Ambient manage gate shared by the three actions: an ambient tenant
|
|
553
|
+
* context is required (no context ⇒ no identity ⇒ fail closed — the
|
|
554
|
+
* suggestion queue is never an anonymous surface), and the caller must hold
|
|
555
|
+
* `fields.policy.manage` unless running under super-admin bypass.
|
|
541
556
|
*/
|
|
542
|
-
async
|
|
543
|
-
const context =
|
|
544
|
-
|
|
545
|
-
await assertOperationPermission({
|
|
557
|
+
async requireManageContext(action) {
|
|
558
|
+
const context = getCurrentTenant();
|
|
559
|
+
if (!context?.tenantId) throw new FieldPolicySuggestionRequestContextError(`${action} requires an ambient tenant context (fail closed): the suggestion queue is scoped to the caller tenant`);
|
|
560
|
+
if (!isSuperAdminBypass()) await assertOperationPermission({
|
|
546
561
|
collection: "fields.policy",
|
|
547
|
-
action:
|
|
548
|
-
db: this.
|
|
549
|
-
tenantId: context
|
|
550
|
-
userId: context
|
|
551
|
-
permissionSet: context
|
|
562
|
+
action: "fields.policy.manage".split(".").at(-1) ?? "manage",
|
|
563
|
+
db: this.db,
|
|
564
|
+
tenantId: context.tenantId,
|
|
565
|
+
userId: context.userId ?? null,
|
|
566
|
+
permissionSet: context.permissions
|
|
567
|
+
});
|
|
568
|
+
return context.tenantId;
|
|
569
|
+
}
|
|
570
|
+
async loadOwnedPendingSuggestion(action, id, tenantId) {
|
|
571
|
+
if (typeof id !== "string" || id.trim() === "") throw new Error(`${action} requires a suggestion "id" string`);
|
|
572
|
+
const suggestion = await this.get(id);
|
|
573
|
+
if (!suggestion) throw new Error(`${action}: no suggestion found for id "${id}"`);
|
|
574
|
+
if (suggestion.tenantId !== tenantId) throw new TenantIsolationError(`Tenant isolation violation in ${action}: the suggestion belongs to another tenant`, {
|
|
575
|
+
tenantId,
|
|
576
|
+
attemptedTenantId: suggestion.tenantId ?? void 0
|
|
552
577
|
});
|
|
578
|
+
if (suggestion.status !== "pending") throw new FieldPolicySuggestionConflictError(action, id);
|
|
579
|
+
return suggestion;
|
|
553
580
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
581
|
+
};
|
|
582
|
+
__publicField$2(FieldPolicySuggestionCollection, "_itemClass", FieldPolicySuggestion);
|
|
583
|
+
FieldPolicySuggestionCollection = __decorateClass$4([smrt({
|
|
584
|
+
conflictColumns: [
|
|
585
|
+
"object_ref",
|
|
586
|
+
"field_name",
|
|
587
|
+
"tenant_id",
|
|
588
|
+
"kind",
|
|
589
|
+
"active_key"
|
|
590
|
+
],
|
|
591
|
+
api: {
|
|
592
|
+
include: [
|
|
593
|
+
"pendingSuggestions",
|
|
594
|
+
"acceptSuggestion",
|
|
595
|
+
"dismissSuggestion"
|
|
596
|
+
],
|
|
597
|
+
principalContext: true,
|
|
598
|
+
routes: {
|
|
599
|
+
pendingSuggestions: {
|
|
600
|
+
scope: "collection",
|
|
601
|
+
method: "POST",
|
|
602
|
+
path: "pending"
|
|
603
|
+
},
|
|
604
|
+
acceptSuggestion: {
|
|
605
|
+
scope: "collection",
|
|
606
|
+
method: "POST",
|
|
607
|
+
path: "accept"
|
|
608
|
+
},
|
|
609
|
+
dismissSuggestion: {
|
|
610
|
+
scope: "collection",
|
|
611
|
+
method: "POST",
|
|
612
|
+
path: "dismiss"
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
cli: false,
|
|
617
|
+
mcp: false
|
|
618
|
+
})], FieldPolicySuggestionCollection);
|
|
619
|
+
function normalizeObjectRefsFilter(rawRefs) {
|
|
620
|
+
if (rawRefs === void 0) return null;
|
|
621
|
+
if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("pendingSuggestions \"objectRefs\" must be a non-empty string array when provided");
|
|
622
|
+
if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("pendingSuggestions objectRefs must be non-empty strings");
|
|
623
|
+
const objectRefs = [...new Set(rawRefs)];
|
|
624
|
+
if (objectRefs.length > MAX_PENDING_OBJECT_REFS) throw new Error(`pendingSuggestions accepts at most ${MAX_PENDING_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
|
|
625
|
+
return objectRefs;
|
|
626
|
+
}
|
|
627
|
+
function normalizeCoolDownMs(raw) {
|
|
628
|
+
if (raw === void 0) return DEFAULT_SUGGESTION_COOL_DOWN_MS;
|
|
629
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) throw new Error("dismissSuggestion coolDownMs must be a finite number");
|
|
630
|
+
return Math.min(Math.max(raw, MIN_SUGGESTION_COOL_DOWN_MS), MAX_SUGGESTION_COOL_DOWN_MS);
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/models/FieldUsageCounter.ts
|
|
634
|
+
var __defProp$3 = Object.defineProperty;
|
|
635
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
636
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
637
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
638
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
639
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
640
|
+
return result;
|
|
641
|
+
};
|
|
642
|
+
var MAX_DISTINCT_USERS_PER_BUCKET = 100;
|
|
643
|
+
var MAX_VALUE_HISTOGRAM_BUCKETS = 25;
|
|
644
|
+
var FIELD_USAGE_PERIOD_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
645
|
+
function fieldUsagePeriodForDate(date) {
|
|
646
|
+
return date.toISOString().slice(0, 10);
|
|
647
|
+
}
|
|
648
|
+
function emptyHistogram() {
|
|
649
|
+
return /* @__PURE__ */ Object.create(null);
|
|
650
|
+
}
|
|
651
|
+
var FieldUsageCounter = class extends SmrtObject {
|
|
652
|
+
objectRef = "";
|
|
653
|
+
fieldName = "";
|
|
654
|
+
tenantId;
|
|
655
|
+
period = "";
|
|
656
|
+
submissionCount = 0;
|
|
657
|
+
setCount = 0;
|
|
658
|
+
distinctUserCount = 0;
|
|
659
|
+
distinctUserIds = "[]";
|
|
660
|
+
distinctUsersOverflowed = false;
|
|
661
|
+
valueHistogram = null;
|
|
662
|
+
valueHistogramOverflowed = false;
|
|
663
|
+
constructor(options = {}) {
|
|
664
|
+
super(options);
|
|
665
|
+
if (options.objectRef !== void 0) this.objectRef = options.objectRef;
|
|
666
|
+
if (options.fieldName !== void 0) this.fieldName = options.fieldName;
|
|
667
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
668
|
+
if (options.period !== void 0) this.period = options.period;
|
|
669
|
+
if (options.submissionCount !== void 0) this.submissionCount = options.submissionCount;
|
|
670
|
+
if (options.setCount !== void 0) this.setCount = options.setCount;
|
|
671
|
+
if (options.distinctUserCount !== void 0) this.distinctUserCount = options.distinctUserCount;
|
|
672
|
+
if (options.distinctUserIds !== void 0) this.distinctUserIds = options.distinctUserIds;
|
|
673
|
+
if (options.distinctUsersOverflowed !== void 0) this.distinctUsersOverflowed = options.distinctUsersOverflowed;
|
|
674
|
+
if (options.valueHistogram !== void 0) this.valueHistogram = options.valueHistogram;
|
|
675
|
+
if (options.valueHistogramOverflowed !== void 0) this.valueHistogramOverflowed = options.valueHistogramOverflowed;
|
|
559
676
|
}
|
|
560
|
-
|
|
677
|
+
/** Parse the stored distinct-user set (guarded; junk parses as empty). */
|
|
678
|
+
getDistinctUserIds() {
|
|
561
679
|
try {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
680
|
+
const parsed = JSON.parse(this.distinctUserIds);
|
|
681
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
|
|
682
|
+
} catch {
|
|
683
|
+
return [];
|
|
565
684
|
}
|
|
566
685
|
}
|
|
567
686
|
/**
|
|
568
|
-
*
|
|
569
|
-
*
|
|
687
|
+
* Add a user to the distinct set, honoring the cap. At the cap the id is
|
|
688
|
+
* NOT added and the overflow marker is set instead, keeping
|
|
689
|
+
* {@link distinctUserCount} an honest lower bound.
|
|
690
|
+
*/
|
|
691
|
+
addDistinctUser(userId) {
|
|
692
|
+
const ids = this.getDistinctUserIds();
|
|
693
|
+
if (ids.includes(userId)) return;
|
|
694
|
+
if (ids.length >= 100) {
|
|
695
|
+
this.distinctUsersOverflowed = true;
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
ids.push(userId);
|
|
699
|
+
this.distinctUserIds = JSON.stringify(ids);
|
|
700
|
+
this.distinctUserCount = ids.length;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Parse the stored histogram (guarded; junk parses as empty).
|
|
570
704
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
* row's contents.
|
|
705
|
+
* Returns a NULL-PROTOTYPE object. Histogram keys are user-supplied ids —
|
|
706
|
+
* an `idType: 'text'` reference may legitimately be `constructor`,
|
|
707
|
+
* `toString`, or `__proto__` — and on a plain object those inherit truthy
|
|
708
|
+
* prototype values (so a missing bucket reads as present) or, for
|
|
709
|
+
* `__proto__`, hit the prototype setter instead of creating an own key.
|
|
710
|
+
* Both would silently corrupt counts. See {@link emptyHistogram}.
|
|
578
711
|
*/
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
if (!
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
return
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
};
|
|
712
|
+
getValueHistogram() {
|
|
713
|
+
const histogram = emptyHistogram();
|
|
714
|
+
if (!this.valueHistogram) return histogram;
|
|
715
|
+
try {
|
|
716
|
+
const parsed = JSON.parse(this.valueHistogram);
|
|
717
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return histogram;
|
|
718
|
+
for (const [key, count] of Object.entries(parsed)) if (typeof count === "number" && Number.isFinite(count) && count > 0) histogram[key] = count;
|
|
719
|
+
return histogram;
|
|
720
|
+
} catch {
|
|
721
|
+
return emptyHistogram();
|
|
722
|
+
}
|
|
601
723
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
724
|
+
/**
|
|
725
|
+
* Record one histogram sample under an already-serialized key, honoring the
|
|
726
|
+
* bucket cap (a NEW key past the cap is dropped and the overflow marker
|
|
727
|
+
* set; existing keys keep counting).
|
|
728
|
+
*
|
|
729
|
+
* Bucket presence is an OWN-key test, never a truthiness/`undefined` read,
|
|
730
|
+
* so prototype-shaped ids behave like any other key.
|
|
731
|
+
*/
|
|
732
|
+
recordHistogramSample(key) {
|
|
733
|
+
if (key.length === 0 || key.length > 64) return;
|
|
734
|
+
const histogram = this.getValueHistogram();
|
|
735
|
+
if (!Object.hasOwn(histogram, key)) {
|
|
736
|
+
if (Object.keys(histogram).length >= 25) {
|
|
737
|
+
this.valueHistogramOverflowed = true;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
histogram[key] = 1;
|
|
741
|
+
} else histogram[key] += 1;
|
|
742
|
+
this.valueHistogram = JSON.stringify(histogram);
|
|
605
743
|
}
|
|
606
744
|
/**
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
745
|
+
* Whether this bucket predates the {@link submissionCount} column (or was
|
|
746
|
+
* corrupted): it records deviations without a total, so no honest dominance
|
|
747
|
+
* ratio can be computed from it. The generation job skips `default`
|
|
748
|
+
* suggestions for any group containing such a bucket.
|
|
610
749
|
*/
|
|
611
|
-
|
|
612
|
-
|
|
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;
|
|
750
|
+
isLegacyBucket() {
|
|
751
|
+
return this.submissionCount < this.setCount;
|
|
619
752
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
this.
|
|
753
|
+
async save() {
|
|
754
|
+
await this.assertRowOwnedByAmbientContext("save");
|
|
755
|
+
this.validateFieldUsageCounter();
|
|
756
|
+
return super.save();
|
|
757
|
+
}
|
|
758
|
+
async delete() {
|
|
759
|
+
await this.assertRowOwnedByAmbientContext("delete");
|
|
760
|
+
await super.delete();
|
|
761
|
+
}
|
|
762
|
+
validateFieldUsageCounter() {
|
|
763
|
+
if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldUsageCounter.objectRef is required");
|
|
764
|
+
if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldUsageCounter.fieldName is required");
|
|
765
|
+
if (!this.tenantId) throw new Error("FieldUsageCounter.tenantId is required");
|
|
766
|
+
if (!FIELD_USAGE_PERIOD_PATTERN.test(this.period)) throw new Error(`FieldUsageCounter.period must be a UTC day bucket (YYYY-MM-DD); got "${this.period}"`);
|
|
767
|
+
if (!Number.isInteger(this.submissionCount) || this.submissionCount < 0) throw new Error("FieldUsageCounter.submissionCount must be a non-negative integer");
|
|
768
|
+
if (!Number.isInteger(this.setCount) || this.setCount < 0) throw new Error("FieldUsageCounter.setCount must be a non-negative integer");
|
|
769
|
+
if (!Number.isInteger(this.distinctUserCount) || this.distinctUserCount < 0) throw new Error("FieldUsageCounter.distinctUserCount must be a non-negative integer");
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Tenant write boundary (the FieldPolicy posture — no class-level
|
|
773
|
+
* `@TenantScoped`, because the learning jobs legitimately operate
|
|
774
|
+
* cross-tenant in trusted execution): inside a non-bypass tenant context a
|
|
775
|
+
* caller may only touch rows of its own tenant; without a context (system/
|
|
776
|
+
* job execution) writes are trusted.
|
|
777
|
+
*
|
|
778
|
+
* Checked against BOTH the in-memory tenant and — for a row that already
|
|
779
|
+
* exists — the PERSISTED one. The persisted check is what makes the boundary
|
|
780
|
+
* real: bucket ids are deterministic and the deriving helper is exported, so
|
|
781
|
+
* a foreign row is trivially addressable, and an in-memory-only check would
|
|
782
|
+
* let a caller load it, re-stamp `tenantId` with its own, and adopt or delete
|
|
783
|
+
* another tenant's counters (the #2047 FieldPolicy pattern).
|
|
784
|
+
*/
|
|
785
|
+
async assertRowOwnedByAmbientContext(operation) {
|
|
786
|
+
const context = getCurrentTenant();
|
|
787
|
+
if (!context || isSuperAdminBypass()) return;
|
|
788
|
+
if (this.tenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldUsageCounter.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${this.tenantId}'`, {
|
|
789
|
+
tenantId: context.tenantId,
|
|
790
|
+
attemptedTenantId: this.tenantId ?? void 0
|
|
791
|
+
});
|
|
792
|
+
const persistedTenantId = await this.getPersistedTenantId();
|
|
793
|
+
if (persistedTenantId !== null && persistedTenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldUsageCounter.${operation}: the persisted row belongs to '${persistedTenantId}'`, {
|
|
794
|
+
tenantId: context.tenantId,
|
|
795
|
+
attemptedTenantId: persistedTenantId
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/** The stored tenant for this row's id; `null` when it is not persisted. */
|
|
799
|
+
async getPersistedTenantId() {
|
|
800
|
+
if (!this.id) return null;
|
|
801
|
+
const existing = await this.db.get(this.tableName, { id: this.id });
|
|
802
|
+
if (!existing) return null;
|
|
803
|
+
const row = existing;
|
|
804
|
+
const value = row.tenantId ?? row.tenant_id;
|
|
805
|
+
return value === void 0 || value === null ? null : String(value);
|
|
631
806
|
}
|
|
632
807
|
};
|
|
633
|
-
__decorateClass$
|
|
634
|
-
__decorateClass$
|
|
635
|
-
__decorateClass$
|
|
636
|
-
__decorateClass$
|
|
637
|
-
__decorateClass$
|
|
638
|
-
__decorateClass$
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
})],
|
|
642
|
-
__decorateClass$
|
|
643
|
-
type: "text",
|
|
644
|
-
nullable: true
|
|
645
|
-
})], FieldPolicy.prototype, "defaultValue", 2);
|
|
646
|
-
__decorateClass$1([field({
|
|
808
|
+
__decorateClass$3([field({ required: true })], FieldUsageCounter.prototype, "objectRef", 2);
|
|
809
|
+
__decorateClass$3([field({ required: true })], FieldUsageCounter.prototype, "fieldName", 2);
|
|
810
|
+
__decorateClass$3([tenantId()], FieldUsageCounter.prototype, "tenantId", 2);
|
|
811
|
+
__decorateClass$3([field({ required: true })], FieldUsageCounter.prototype, "period", 2);
|
|
812
|
+
__decorateClass$3([field({ type: "integer" })], FieldUsageCounter.prototype, "submissionCount", 2);
|
|
813
|
+
__decorateClass$3([field({ type: "integer" })], FieldUsageCounter.prototype, "setCount", 2);
|
|
814
|
+
__decorateClass$3([field({ type: "integer" })], FieldUsageCounter.prototype, "distinctUserCount", 2);
|
|
815
|
+
__decorateClass$3([field({ type: "text" })], FieldUsageCounter.prototype, "distinctUserIds", 2);
|
|
816
|
+
__decorateClass$3([field({ type: "boolean" })], FieldUsageCounter.prototype, "distinctUsersOverflowed", 2);
|
|
817
|
+
__decorateClass$3([field({
|
|
647
818
|
type: "text",
|
|
648
819
|
nullable: true
|
|
649
|
-
})],
|
|
650
|
-
__decorateClass$
|
|
651
|
-
|
|
652
|
-
|
|
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",
|
|
820
|
+
})], FieldUsageCounter.prototype, "valueHistogram", 2);
|
|
821
|
+
__decorateClass$3([field({ type: "boolean" })], FieldUsageCounter.prototype, "valueHistogramOverflowed", 2);
|
|
822
|
+
FieldUsageCounter = __decorateClass$3([smrt({
|
|
823
|
+
tableName: "_smrt_field_usage_counters",
|
|
669
824
|
conflictColumns: [
|
|
670
825
|
"object_ref",
|
|
671
826
|
"field_name",
|
|
672
|
-
"
|
|
673
|
-
"
|
|
827
|
+
"tenant_id",
|
|
828
|
+
"period"
|
|
674
829
|
],
|
|
675
|
-
api: {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
830
|
+
api: { include: [] },
|
|
831
|
+
cli: false,
|
|
832
|
+
mcp: { include: [] }
|
|
833
|
+
})], FieldUsageCounter);
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/models/FieldUsageReportReceipt.ts
|
|
836
|
+
var __defProp$2 = Object.defineProperty;
|
|
837
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
838
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
839
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
840
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
841
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
842
|
+
return result;
|
|
843
|
+
};
|
|
844
|
+
var FieldUsageReportReceipt = class extends SmrtObject {
|
|
845
|
+
tenantId;
|
|
846
|
+
userId = "";
|
|
847
|
+
objectRef = "";
|
|
848
|
+
fieldName = "";
|
|
849
|
+
period = "";
|
|
850
|
+
constructor(options = {}) {
|
|
851
|
+
super(options);
|
|
852
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
853
|
+
if (options.userId !== void 0) this.userId = options.userId;
|
|
854
|
+
if (options.objectRef !== void 0) this.objectRef = options.objectRef;
|
|
855
|
+
if (options.fieldName !== void 0) this.fieldName = options.fieldName;
|
|
856
|
+
if (options.period !== void 0) this.period = options.period;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
__decorateClass$2([tenantId()], FieldUsageReportReceipt.prototype, "tenantId", 2);
|
|
860
|
+
__decorateClass$2([crossPackageRef("@happyvertical/smrt-users:User")], FieldUsageReportReceipt.prototype, "userId", 2);
|
|
861
|
+
__decorateClass$2([field({ required: true })], FieldUsageReportReceipt.prototype, "objectRef", 2);
|
|
862
|
+
__decorateClass$2([field({ required: true })], FieldUsageReportReceipt.prototype, "fieldName", 2);
|
|
863
|
+
__decorateClass$2([field({ required: true })], FieldUsageReportReceipt.prototype, "period", 2);
|
|
864
|
+
FieldUsageReportReceipt = __decorateClass$2([smrt({
|
|
865
|
+
tableName: "_smrt_field_usage_report_receipts",
|
|
866
|
+
conflictColumns: [
|
|
867
|
+
"tenant_id",
|
|
868
|
+
"user_id",
|
|
869
|
+
"object_ref",
|
|
870
|
+
"field_name",
|
|
871
|
+
"period"
|
|
872
|
+
],
|
|
873
|
+
api: { include: [] },
|
|
874
|
+
cli: false,
|
|
691
875
|
mcp: { include: [] }
|
|
692
|
-
})],
|
|
876
|
+
})], FieldUsageReportReceipt);
|
|
693
877
|
//#endregion
|
|
694
|
-
//#region src/collections/
|
|
695
|
-
var __defProp = Object.defineProperty;
|
|
696
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
697
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
878
|
+
//#region src/collections/FieldUsageCounterCollection.ts
|
|
879
|
+
var __defProp$1 = Object.defineProperty;
|
|
880
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
881
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
|
|
698
882
|
enumerable: true,
|
|
699
883
|
configurable: true,
|
|
700
884
|
writable: true,
|
|
701
885
|
value
|
|
702
886
|
}) : obj[key] = value;
|
|
703
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
704
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
887
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
888
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
705
889
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
706
|
-
if (kind && result) __defProp(target, key, result);
|
|
890
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
707
891
|
return result;
|
|
708
892
|
};
|
|
709
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
893
|
+
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
894
|
+
function usageValuesEqual(a, b) {
|
|
895
|
+
if (a === b) return true;
|
|
896
|
+
try {
|
|
897
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
var MAX_USAGE_REPORT_ENTRIES = 100;
|
|
903
|
+
var FieldUsageRequestContextError = class extends TenantIsolationError {
|
|
904
|
+
httpStatus = 403;
|
|
905
|
+
status = 403;
|
|
906
|
+
constructor(message, details) {
|
|
907
|
+
super(message, details);
|
|
908
|
+
this.name = "FieldUsageRequestContextError";
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
var FieldUsageCounterCollection = class extends SmrtCollection {
|
|
713
912
|
/**
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
913
|
+
* Record a batch of field submissions into the current UTC-day counters.
|
|
914
|
+
*
|
|
915
|
+
* Identity is AMBIENT-ONLY (the resolveBatch posture): the tenant AND the
|
|
916
|
+
* user come exclusively from the tenant context established by the app's
|
|
917
|
+
* auth hook — the request body cannot attribute usage to another tenant or
|
|
918
|
+
* user. Both are REQUIRED and the call fails closed without them (#2047
|
|
919
|
+
* round-3 posture):
|
|
920
|
+
*
|
|
921
|
+
* - No ambient tenant ⇒ the report is unattributable.
|
|
922
|
+
* - No ambient USER ⇒ the caller is an unauthenticated principal (a
|
|
923
|
+
* deployment that resolves the tenant from host/header alone). Such
|
|
924
|
+
* callers could otherwise inflate counts and histograms without bound
|
|
925
|
+
* (the batch cap is per call), and `distinctUsers` — the promote signal —
|
|
926
|
+
* is meaningless without a principal. Consequence, accepted deliberately:
|
|
927
|
+
* ANONYMOUS/PUBLIC FORMS DO NOT CONTRIBUTE USAGE. The learning loop is
|
|
928
|
+
* for authenticated org users.
|
|
929
|
+
*
|
|
930
|
+
* Any authenticated in-tenant principal may report (no manage slug needed).
|
|
931
|
+
* A durable receipt admits at most one `(tenant, user, object, field, UTC
|
|
932
|
+
* day)` contribution, and the batch stays bounded per call
|
|
933
|
+
* ({@link MAX_USAGE_REPORT_ENTRIES}).
|
|
934
|
+
*
|
|
935
|
+
* Server-side derivation (the client is never the authority):
|
|
936
|
+
* - Fields are validated against the live `ObjectRegistry`; unknown or
|
|
937
|
+
* non-usage-addressable entries (system, relationship pseudo-fields, STI
|
|
938
|
+
* meta storage, transient) are DROPPED and counted, never failing the
|
|
939
|
+
* batch (stale clients after a redeploy are expected).
|
|
940
|
+
* - Sensitivity comes from BOTH `field.sensitive` and `field._meta.sensitive`
|
|
941
|
+
* (and `readPermission` in both places): such fields are recorded
|
|
942
|
+
* COUNT-ONLY — their values are never persisted anywhere in usage data,
|
|
943
|
+
* default-matching or not.
|
|
944
|
+
* - DEVIATION (`setCount`, distinct users) is decided by comparing the
|
|
945
|
+
* submitted value against the default RESOLVED HERE for the calling
|
|
946
|
+
* identity — never against a client claim. Every accepted entry also
|
|
947
|
+
* increments `submissionCount`, the dominance denominator.
|
|
948
|
+
* - Values are histogrammed only for low-cardinality field types
|
|
949
|
+
* (`boolean`, `foreignKey`, `crossPackageRef`) with type-checked samples;
|
|
950
|
+
* free text is never histogrammed.
|
|
951
|
+
*
|
|
952
|
+
* Bounded trust in `matchedDefault`: an entry may omit `value` entirely
|
|
953
|
+
* (`collectFieldUsageEntries({ includeValues: false })` — apps that prefer
|
|
954
|
+
* no value transit). Only then is the entry's `matchedDefault` flag read, to
|
|
955
|
+
* decide the deviation bit alone. It can never create a histogram entry,
|
|
956
|
+
* never bypass the count-only rail, and never manufacture distinct users
|
|
957
|
+
* beyond the caller's own id. The once-per-field/day receipt means a lying
|
|
958
|
+
* client gets only one such contribution, so repeated POSTs cannot inflate
|
|
959
|
+
* thresholds or steer a `default` suggestion.
|
|
717
960
|
*/
|
|
718
|
-
async
|
|
719
|
-
const
|
|
720
|
-
const countObjectRefs = normalizeAuditRefs(options.countObjectRefs ?? objectRefs, "countObjectRefs", 200);
|
|
961
|
+
async reportUsage(options = {}) {
|
|
962
|
+
const entries = validateEntriesInput(options.entries);
|
|
721
963
|
const context = getCurrentTenant();
|
|
722
|
-
const tenantId = context?.tenantId
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
action: "fields.policy.manage".split(".").at(-1) ?? "manage"
|
|
735
|
-
}) : { allowed: false };
|
|
736
|
-
const personalize = userId ? await checkOperationPermission({
|
|
737
|
-
...permissionOptions,
|
|
738
|
-
action: "fields.policy.personalize".split(".").at(-1) ?? "personalize"
|
|
739
|
-
}) : { allowed: false };
|
|
740
|
-
const caller = {
|
|
741
|
-
tenantId,
|
|
742
|
-
userId,
|
|
743
|
-
canManageOrg: manage.allowed,
|
|
744
|
-
canPersonalize: personalize.allowed && userId !== null
|
|
745
|
-
};
|
|
746
|
-
if (!caller.canManageOrg || tenantId === null) return emptyAudit(caller);
|
|
747
|
-
if (options.summaryOnly === true) return emptyAudit(caller);
|
|
748
|
-
const fieldMaps = /* @__PURE__ */ new Map();
|
|
749
|
-
const fieldMapFor = (objectRef) => {
|
|
750
|
-
let loaded = fieldMaps.get(objectRef);
|
|
751
|
-
if (!loaded) {
|
|
752
|
-
loaded = getObjectFieldMap(objectRef).catch(() => null);
|
|
753
|
-
fieldMaps.set(objectRef, loaded);
|
|
964
|
+
const tenantId = context?.tenantId;
|
|
965
|
+
if (!tenantId) throw new FieldUsageRequestContextError("reportUsage requires an ambient tenant context: usage is unattributable without one, so the call fails closed");
|
|
966
|
+
const userId = context.userId;
|
|
967
|
+
if (!userId) throw new FieldUsageRequestContextError("reportUsage requires an ambient AUTHENTICATED user: an unauthenticated caller could inflate counters without bound and distinct-user thresholds are meaningless without a principal, so the call fails closed (anonymous forms do not contribute usage)", { tenantId });
|
|
968
|
+
const period = fieldUsagePeriodForDate(/* @__PURE__ */ new Date());
|
|
969
|
+
const receipts = await FieldUsageReportReceiptCollection.create({ db: this.db });
|
|
970
|
+
const groups = /* @__PURE__ */ new Map();
|
|
971
|
+
for (const entry of entries) {
|
|
972
|
+
let byField = groups.get(entry.objectRef);
|
|
973
|
+
if (!byField) {
|
|
974
|
+
byField = /* @__PURE__ */ new Map();
|
|
975
|
+
groups.set(entry.objectRef, byField);
|
|
754
976
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const fields = await fieldMapFor(row.objectRef);
|
|
759
|
-
if (!fields) return "unknown-object";
|
|
760
|
-
const definition = fields.get(row.fieldName);
|
|
761
|
-
if (!definition) return "unknown-field";
|
|
762
|
-
if (isSensitiveField(definition) || getFieldReadPermission(definition) !== void 0 || isTransientField(definition)) return "gated";
|
|
763
|
-
return isPolicyAddressableField(definition) ? "ok" : "excluded-field";
|
|
764
|
-
};
|
|
765
|
-
const [appRows, orgRows, ownUserRows] = objectRefs.length ? await Promise.all([
|
|
766
|
-
this.list({ where: {
|
|
767
|
-
scopeType: "app",
|
|
768
|
-
"objectRef in": objectRefs
|
|
769
|
-
} }),
|
|
770
|
-
this.list({ where: {
|
|
771
|
-
scopeType: "tenant",
|
|
772
|
-
tenantId,
|
|
773
|
-
"objectRef in": objectRefs
|
|
774
|
-
} }),
|
|
775
|
-
userId ? this.list({ where: {
|
|
776
|
-
scopeType: "user",
|
|
777
|
-
userId,
|
|
778
|
-
"objectRef in": objectRefs
|
|
779
|
-
} }) : Promise.resolve([])
|
|
780
|
-
]) : [
|
|
781
|
-
[],
|
|
782
|
-
[],
|
|
783
|
-
[]
|
|
784
|
-
];
|
|
785
|
-
const audit = {
|
|
786
|
-
...emptyAudit(caller),
|
|
787
|
-
policies: options.countsOnly ? {} : await this.resolveAuditPolicies(objectRefs, tenantId, fieldMapFor)
|
|
788
|
-
};
|
|
789
|
-
const append = async (row, target, driftPrunable) => {
|
|
790
|
-
const state = await classify(row);
|
|
791
|
-
if (state === "ok") target.push(toAuditRow(row));
|
|
792
|
-
else if (state !== "gated") audit.driftRows.push({
|
|
793
|
-
...toDriftIdentity(row),
|
|
794
|
-
reason: state,
|
|
795
|
-
prunable: driftPrunable
|
|
796
|
-
});
|
|
797
|
-
};
|
|
798
|
-
for (const row of orgRows) await append(row, audit.orgRows, true);
|
|
799
|
-
for (const row of appRows) await append(row, audit.appRows, isSuperAdminBypass());
|
|
800
|
-
for (const row of ownUserRows) {
|
|
801
|
-
const state = await classify(row);
|
|
802
|
-
if (state !== "ok" && state !== "gated") audit.driftRows.push({
|
|
803
|
-
...toDriftIdentity(row),
|
|
804
|
-
reason: state,
|
|
805
|
-
prunable: caller.canPersonalize
|
|
806
|
-
});
|
|
977
|
+
const samples = byField.get(entry.fieldName);
|
|
978
|
+
if (samples) samples.push(entry);
|
|
979
|
+
else byField.set(entry.fieldName, [entry]);
|
|
807
980
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
tenantId
|
|
818
|
-
},
|
|
819
|
-
limit: 100
|
|
820
|
-
}),
|
|
821
|
-
userId ? this.list({
|
|
822
|
-
where: {
|
|
823
|
-
scopeType: "user",
|
|
824
|
-
userId
|
|
825
|
-
},
|
|
826
|
-
limit: 100
|
|
827
|
-
}) : Promise.resolve([])
|
|
828
|
-
]);
|
|
829
|
-
const knownIds = new Set([
|
|
830
|
-
...appRows,
|
|
831
|
-
...orgRows,
|
|
832
|
-
...ownUserRows
|
|
833
|
-
].map((row) => String(row.id)));
|
|
834
|
-
for (const [rows, prunable] of [
|
|
835
|
-
[driftAppRows, isSuperAdminBypass()],
|
|
836
|
-
[driftOrgRows, true],
|
|
837
|
-
[driftUserRows, caller.canPersonalize]
|
|
838
|
-
]) for (const row of rows) {
|
|
839
|
-
if (knownIds.has(String(row.id))) continue;
|
|
840
|
-
const state = await classify(row);
|
|
841
|
-
if (state !== "ok" && state !== "gated") audit.driftRows.push({
|
|
842
|
-
...toDriftIdentity(row),
|
|
843
|
-
reason: state,
|
|
844
|
-
prunable
|
|
845
|
-
});
|
|
981
|
+
let accepted = 0;
|
|
982
|
+
let dropped = 0;
|
|
983
|
+
for (const [objectRef, byField] of groups) {
|
|
984
|
+
let fieldMap;
|
|
985
|
+
try {
|
|
986
|
+
fieldMap = await getObjectFieldMap(objectRef);
|
|
987
|
+
} catch {
|
|
988
|
+
for (const samples of byField.values()) dropped += samples.length;
|
|
989
|
+
continue;
|
|
846
990
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
where: {
|
|
852
|
-
scopeType: "user",
|
|
853
|
-
"objectRef in": countObjectRefs,
|
|
854
|
-
"userId in": memberIds
|
|
855
|
-
},
|
|
856
|
-
select: ["objectRef", "fieldName"]
|
|
857
|
-
}) : [];
|
|
858
|
-
for (const row of projected) {
|
|
859
|
-
if (await classify(row) !== "ok") continue;
|
|
860
|
-
const byField = audit.userOverrideCounts[row.objectRef] ?? {};
|
|
861
|
-
audit.userOverrideCounts[row.objectRef] = byField;
|
|
862
|
-
byField[row.fieldName] = (byField[row.fieldName] ?? 0) + 1;
|
|
863
|
-
}
|
|
864
|
-
const { resolveSurvivingTenantChainIds } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
865
|
-
const ancestorIds = (await resolveSurvivingTenantChainIds(tenantId, { db: this.db })).filter((id) => id !== tenantId);
|
|
866
|
-
if (ancestorIds.length && objectRefs.length) {
|
|
867
|
-
const ancestors = await this.list({ where: {
|
|
868
|
-
scopeType: "tenant",
|
|
869
|
-
"tenantId in": ancestorIds,
|
|
870
|
-
"objectRef in": objectRefs
|
|
871
|
-
} });
|
|
872
|
-
for (const row of ancestors) {
|
|
873
|
-
if (await classify(row) !== "ok") continue;
|
|
874
|
-
const names = audit.inheritedOrgKeys[row.objectRef] ?? [];
|
|
875
|
-
audit.inheritedOrgKeys[row.objectRef] = names;
|
|
876
|
-
if (!names.includes(row.fieldName)) names.push(row.fieldName);
|
|
991
|
+
const resolvedFields = await this.resolveDefaultsForCaller(objectRef, tenantId, userId);
|
|
992
|
+
if (!resolvedFields) {
|
|
993
|
+
for (const samples of byField.values()) dropped += samples.length;
|
|
994
|
+
continue;
|
|
877
995
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const byTenant = /* @__PURE__ */ new Map();
|
|
917
|
-
if (tenantIds.length === 0) return byTenant;
|
|
918
|
-
const rows = await this.list({ where: {
|
|
919
|
-
objectRef,
|
|
920
|
-
scopeType: "tenant",
|
|
921
|
-
"tenantId in": tenantIds
|
|
922
|
-
} });
|
|
923
|
-
for (const row of rows) {
|
|
924
|
-
if (!row.tenantId) continue;
|
|
925
|
-
let byField = byTenant.get(row.tenantId);
|
|
926
|
-
if (!byField) {
|
|
927
|
-
byField = /* @__PURE__ */ new Map();
|
|
928
|
-
byTenant.set(row.tenantId, byField);
|
|
996
|
+
for (const [fieldName, samples] of byField) {
|
|
997
|
+
const fieldDef = fieldMap.get(fieldName);
|
|
998
|
+
if (!fieldDef || !isUsageAddressableField(fieldDef)) {
|
|
999
|
+
dropped += samples.length;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (!await receipts.claim({
|
|
1003
|
+
tenantId,
|
|
1004
|
+
userId,
|
|
1005
|
+
objectRef,
|
|
1006
|
+
fieldName,
|
|
1007
|
+
period
|
|
1008
|
+
})) continue;
|
|
1009
|
+
const sample = samples[0];
|
|
1010
|
+
const histogramEligible = !(isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0) && isHistogramEligibleField(fieldDef);
|
|
1011
|
+
const resolved = resolvedFields[fieldName];
|
|
1012
|
+
const hasDefault = resolved?.hasDefault === true;
|
|
1013
|
+
const defaultValue = hasDefault ? resolved?.defaultValue : void 0;
|
|
1014
|
+
let deviations = 0;
|
|
1015
|
+
const histogramKeys = [];
|
|
1016
|
+
if ("value" in sample) {
|
|
1017
|
+
if (!hasDefault || !usageValuesEqual(sample.value, defaultValue)) deviations += 1;
|
|
1018
|
+
if (histogramEligible) {
|
|
1019
|
+
const key = serializeHistogramSample(fieldDef, sample.value);
|
|
1020
|
+
if (key !== null) histogramKeys.push(key);
|
|
1021
|
+
}
|
|
1022
|
+
} else if (sample.matchedDefault !== true) deviations += 1;
|
|
1023
|
+
await this.mergeIntoBucket({
|
|
1024
|
+
objectRef,
|
|
1025
|
+
fieldName,
|
|
1026
|
+
tenantId,
|
|
1027
|
+
period,
|
|
1028
|
+
userId,
|
|
1029
|
+
submissionCount: 1,
|
|
1030
|
+
deviationCount: deviations,
|
|
1031
|
+
histogramKeys
|
|
1032
|
+
});
|
|
1033
|
+
accepted += 1;
|
|
929
1034
|
}
|
|
930
|
-
byField.set(row.fieldName, row);
|
|
931
1035
|
}
|
|
932
|
-
return
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const rows = await this.list({ where: {
|
|
937
|
-
objectRef,
|
|
938
|
-
scopeType: "user",
|
|
939
|
-
userId
|
|
940
|
-
} });
|
|
941
|
-
const byField = /* @__PURE__ */ new Map();
|
|
942
|
-
for (const row of rows) byField.set(row.fieldName, row);
|
|
943
|
-
return byField;
|
|
1036
|
+
return {
|
|
1037
|
+
accepted,
|
|
1038
|
+
dropped
|
|
1039
|
+
};
|
|
944
1040
|
}
|
|
945
1041
|
/**
|
|
946
|
-
*
|
|
947
|
-
*
|
|
1042
|
+
* The defaults the CALLER's forms would have prefilled — the deviation
|
|
1043
|
+
* authority. Resolved for the ambient `(tenant, user)` identity (the
|
|
1044
|
+
* resolver's own TTL cache keeps repeat batches cheap).
|
|
948
1045
|
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
* Server-side consumers wanting explicit identities call
|
|
953
|
-
* `resolveFieldPolicy()` directly instead.
|
|
954
|
-
*
|
|
955
|
-
* Field gating mirrors the REST serializer's derivation (`sensitive` /
|
|
956
|
-
* `readPermission` read from both the top level and `_meta`): sensitive and
|
|
957
|
-
* read-permission-gated fields are ABSENT from the response for every
|
|
958
|
-
* caller (the generated action route cannot convey per-caller grants to
|
|
959
|
-
* this method, so gated fields fail closed), and transient fields are
|
|
960
|
-
* stripped for parity with generated client field definitions.
|
|
1046
|
+
* Ingestion is fire-and-forget, so a stored-layer read failure degrades to
|
|
1047
|
+
* the CODE-SEED-only resolution (no db) rather than failing the request;
|
|
1048
|
+
* `null` (both attempts failed) makes the caller drop the group.
|
|
961
1049
|
*/
|
|
962
|
-
async
|
|
963
|
-
const rawRefs = options.objectRefs;
|
|
964
|
-
if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("resolveBatch requires a non-empty \"objectRefs\" string array");
|
|
965
|
-
if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("resolveBatch objectRefs must be non-empty strings");
|
|
966
|
-
const objectRefs = [...new Set(rawRefs)];
|
|
967
|
-
if (objectRefs.length > MAX_BATCH_OBJECT_REFS) throw new Error(`resolveBatch accepts at most ${MAX_BATCH_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
|
|
968
|
-
const context = getCurrentTenant();
|
|
969
|
-
const tenantId = context?.tenantId ?? null;
|
|
970
|
-
const userId = context?.userId ?? null;
|
|
1050
|
+
async resolveDefaultsForCaller(objectRef, tenantId, userId) {
|
|
971
1051
|
const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const resolved = await resolveFieldPolicy(objectRef, {
|
|
1052
|
+
try {
|
|
1053
|
+
return (await resolveFieldPolicy(objectRef, {
|
|
975
1054
|
tenantId,
|
|
976
1055
|
userId,
|
|
977
1056
|
db: this.db
|
|
978
|
-
});
|
|
979
|
-
|
|
1057
|
+
})).fields;
|
|
1058
|
+
} catch {
|
|
1059
|
+
try {
|
|
1060
|
+
return (await resolveFieldPolicy(objectRef, {
|
|
1061
|
+
tenantId,
|
|
1062
|
+
userId
|
|
1063
|
+
})).fields;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
980
1067
|
}
|
|
981
|
-
return { policies };
|
|
982
1068
|
}
|
|
983
1069
|
/**
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
* edit, never arbitrary tenant or user policy rows.
|
|
1070
|
+
* Counter rows within a period window (inclusive bounds, lexicographic ISO
|
|
1071
|
+
* day comparison), optionally restricted to one tenant — the read the
|
|
1072
|
+
* suggestion-generation job consumes.
|
|
988
1073
|
*/
|
|
989
|
-
async
|
|
990
|
-
const
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
tenantId: context?.tenantId ?? null,
|
|
997
|
-
userId: context?.userId ?? null,
|
|
998
|
-
permissionSet: context?.permissions,
|
|
999
|
-
onDeny: "return"
|
|
1000
|
-
};
|
|
1001
|
-
const manage = await checkOperationPermission({
|
|
1002
|
-
...permissionOptions,
|
|
1003
|
-
action: "fields.policy.manage".split(".").at(-1) ?? "manage"
|
|
1004
|
-
});
|
|
1005
|
-
const personalize = await checkOperationPermission({
|
|
1006
|
-
...permissionOptions,
|
|
1007
|
-
action: "fields.policy.personalize".split(".").at(-1) ?? "personalize"
|
|
1074
|
+
async listWindow(options) {
|
|
1075
|
+
const where = { "period >=": options.fromPeriod };
|
|
1076
|
+
if (options.toPeriod) where["period <="] = options.toPeriod;
|
|
1077
|
+
if (options.tenantId) where.tenantId = options.tenantId;
|
|
1078
|
+
return this.list({
|
|
1079
|
+
where,
|
|
1080
|
+
orderBy: "period ASC"
|
|
1008
1081
|
});
|
|
1009
|
-
const personalizeAllowed = personalize.allowed && typeof context?.userId === "string" && context.userId.length > 0;
|
|
1010
|
-
if (!manage.allowed && !personalizeAllowed) return {
|
|
1011
|
-
code: "permission_denied",
|
|
1012
|
-
message: `Permission denied for '${personalize.permission ?? "fields.policy.manage"}'.`,
|
|
1013
|
-
ok: false,
|
|
1014
|
-
status: 403
|
|
1015
|
-
};
|
|
1016
|
-
const { resolveFieldPolicyExplained } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
1017
|
-
const resolved = await resolveFieldPolicyExplained(objectRef, {
|
|
1018
|
-
tenantId: context?.tenantId ?? null,
|
|
1019
|
-
userId: context?.userId ?? null,
|
|
1020
|
-
db: this.db
|
|
1021
|
-
});
|
|
1022
|
-
const policy = await this.gateExplainedForEditorResponse(resolved, {
|
|
1023
|
-
manage: manage.allowed,
|
|
1024
|
-
personalize: personalizeAllowed
|
|
1025
|
-
});
|
|
1026
|
-
const fieldNames = new Set(Object.keys(policy.fields));
|
|
1027
|
-
const personalLowerDefaultUsable = personalizeAllowed ? await this.getPersonalLowerDefaultUsability(objectRef, context?.tenantId ?? null, fieldNames) : {};
|
|
1028
|
-
const appRows = manage.allowed ? this.toEditorRows(await this.getAppRows(objectRef), fieldNames) : [];
|
|
1029
|
-
const tenantRows = manage.allowed && context?.tenantId ? this.toEditorRows((await this.getTenantRows(objectRef, [context.tenantId])).get(context.tenantId) ?? /* @__PURE__ */ new Map(), fieldNames) : [];
|
|
1030
|
-
const userRows = personalizeAllowed && context?.userId ? this.toEditorRows(await this.getUserRows(objectRef, context.userId), fieldNames) : [];
|
|
1031
|
-
return {
|
|
1032
|
-
capabilities: {
|
|
1033
|
-
manage: manage.allowed,
|
|
1034
|
-
personalize: personalizeAllowed
|
|
1035
|
-
},
|
|
1036
|
-
personalLowerDefaultUsable,
|
|
1037
|
-
policy,
|
|
1038
|
-
rows: {
|
|
1039
|
-
app: appRows,
|
|
1040
|
-
tenant: tenantRows,
|
|
1041
|
-
user: userRows
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
1082
|
}
|
|
1045
1083
|
/**
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1084
|
+
* Idempotent read-modify-write merge into the deterministic day bucket.
|
|
1085
|
+
* Counters are approximate by design: concurrent cross-process merges may
|
|
1086
|
+
* lose an increment (documented on the model), while the deterministic id
|
|
1087
|
+
* plus the natural-key unique index keep concurrent creates converging on
|
|
1088
|
+
* one row.
|
|
1051
1089
|
*/
|
|
1052
|
-
async
|
|
1053
|
-
const
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
db: this.db
|
|
1090
|
+
async mergeIntoBucket(options) {
|
|
1091
|
+
const id = await fieldUsageCounterId(options.tenantId, options.objectRef, options.fieldName, options.period);
|
|
1092
|
+
const existing = await this.get(id);
|
|
1093
|
+
const counter = existing ?? new FieldUsageCounter({
|
|
1094
|
+
db: this.db,
|
|
1095
|
+
id,
|
|
1096
|
+
objectRef: options.objectRef,
|
|
1097
|
+
fieldName: options.fieldName,
|
|
1098
|
+
tenantId: options.tenantId,
|
|
1099
|
+
period: options.period
|
|
1057
1100
|
});
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const fieldMap = await getObjectFieldMap(resolved.objectRef);
|
|
1065
|
-
const fields = {};
|
|
1066
|
-
for (const [fieldName, policy] of Object.entries(resolved.fields)) {
|
|
1067
|
-
const fieldDef = fieldMap.get(fieldName);
|
|
1068
|
-
if (!fieldDef) continue;
|
|
1069
|
-
if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
|
|
1070
|
-
fields[fieldName] = policy;
|
|
1071
|
-
}
|
|
1072
|
-
return {
|
|
1073
|
-
objectRef: resolved.objectRef,
|
|
1074
|
-
fields
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
async gateExplainedForEditorResponse(resolved, capabilities, includeCode = false) {
|
|
1078
|
-
const fieldMap = await getObjectFieldMap(resolved.objectRef);
|
|
1079
|
-
const fields = {};
|
|
1080
|
-
const layers = {};
|
|
1081
|
-
for (const [fieldName, policy] of Object.entries(resolved.fields)) {
|
|
1082
|
-
const fieldDef = fieldMap.get(fieldName);
|
|
1083
|
-
if (!fieldDef || isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
|
|
1084
|
-
fields[fieldName] = policy;
|
|
1085
|
-
layers[fieldName] = (resolved.layers[fieldName] ?? []).filter((layer) => includeCode && layer.layer === "code" || capabilities.manage && (layer.layer === "app" || layer.layer === "tenant") || capabilities.personalize && layer.layer === "user");
|
|
1086
|
-
}
|
|
1087
|
-
return {
|
|
1088
|
-
objectRef: resolved.objectRef,
|
|
1089
|
-
fields,
|
|
1090
|
-
layers
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
toEditorRows(rows, allowedFieldNames) {
|
|
1094
|
-
return Array.from(rows.values()).filter((row) => allowedFieldNames.has(row.fieldName)).map((row) => ({
|
|
1095
|
-
defaultValue: row.defaultValue,
|
|
1096
|
-
displayOrder: row.displayOrder,
|
|
1097
|
-
fieldName: row.fieldName,
|
|
1098
|
-
help: row.help,
|
|
1099
|
-
id: String(row.id),
|
|
1100
|
-
label: row.label,
|
|
1101
|
-
locked: row.locked,
|
|
1102
|
-
scopeType: row.scopeType,
|
|
1103
|
-
tenantId: row.tenantId,
|
|
1104
|
-
updatedBy: row.updatedBy,
|
|
1105
|
-
userId: row.userId,
|
|
1106
|
-
visibility: row.visibility
|
|
1107
|
-
})).sort((left, right) => left.fieldName.localeCompare(right.fieldName));
|
|
1101
|
+
counter.submissionCount += options.submissionCount;
|
|
1102
|
+
counter.setCount += options.deviationCount;
|
|
1103
|
+
if (options.deviationCount > 0) counter.addDistinctUser(options.userId);
|
|
1104
|
+
for (const key of options.histogramKeys) counter.recordHistogramSample(key);
|
|
1105
|
+
if (!existing) await counter.initialize();
|
|
1106
|
+
await counter.save();
|
|
1108
1107
|
}
|
|
1109
1108
|
};
|
|
1110
|
-
__publicField(
|
|
1111
|
-
|
|
1109
|
+
__publicField$1(FieldUsageCounterCollection, "_itemClass", FieldUsageCounter);
|
|
1110
|
+
FieldUsageCounterCollection = __decorateClass$1([smrt({
|
|
1112
1111
|
conflictColumns: [
|
|
1113
1112
|
"object_ref",
|
|
1114
1113
|
"field_name",
|
|
1115
|
-
"
|
|
1116
|
-
"
|
|
1114
|
+
"tenant_id",
|
|
1115
|
+
"period"
|
|
1117
1116
|
],
|
|
1118
1117
|
api: {
|
|
1119
|
-
include: [
|
|
1120
|
-
"create",
|
|
1121
|
-
"update",
|
|
1122
|
-
"delete",
|
|
1123
|
-
"resolveBatch",
|
|
1124
|
-
"getEditorState",
|
|
1125
|
-
"policyAudit"
|
|
1126
|
-
],
|
|
1118
|
+
include: ["reportUsage"],
|
|
1127
1119
|
principalContext: true,
|
|
1128
|
-
routes: {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
},
|
|
1134
|
-
getEditorState: {
|
|
1135
|
-
scope: "collection",
|
|
1136
|
-
method: "POST",
|
|
1137
|
-
path: "editor-state"
|
|
1138
|
-
},
|
|
1139
|
-
policyAudit: {
|
|
1140
|
-
scope: "collection",
|
|
1141
|
-
method: "POST",
|
|
1142
|
-
path: "policy-audit"
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1120
|
+
routes: { reportUsage: {
|
|
1121
|
+
scope: "collection",
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
path: "report"
|
|
1124
|
+
} }
|
|
1145
1125
|
},
|
|
1146
1126
|
cli: false,
|
|
1147
1127
|
mcp: false
|
|
1148
|
-
})],
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1128
|
+
})], FieldUsageCounterCollection);
|
|
1129
|
+
var FieldUsageReportReceiptCollection = class extends SmrtCollection {
|
|
1130
|
+
async claim(options) {
|
|
1131
|
+
const id = await fieldUsageReportReceiptId(options.tenantId, options.userId, options.objectRef, options.fieldName, options.period);
|
|
1132
|
+
if (await this.get(id)) return false;
|
|
1133
|
+
try {
|
|
1134
|
+
await this.create({
|
|
1135
|
+
...options,
|
|
1136
|
+
id,
|
|
1137
|
+
_insertOnly: true
|
|
1138
|
+
});
|
|
1139
|
+
return true;
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (await this.get(id)) return false;
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
__publicField$1(FieldUsageReportReceiptCollection, "_itemClass", FieldUsageReportReceipt);
|
|
1147
|
+
FieldUsageReportReceiptCollection = __decorateClass$1([smrt({
|
|
1148
|
+
conflictColumns: [
|
|
1149
|
+
"tenant_id",
|
|
1150
|
+
"user_id",
|
|
1151
|
+
"object_ref",
|
|
1152
|
+
"field_name",
|
|
1153
|
+
"period"
|
|
1154
|
+
],
|
|
1155
|
+
api: false,
|
|
1156
|
+
cli: false,
|
|
1157
|
+
mcp: false
|
|
1158
|
+
})], FieldUsageReportReceiptCollection);
|
|
1159
|
+
async function fieldUsageCounterId(tenantId, objectRef, fieldName, period) {
|
|
1160
|
+
return deterministicFieldsUuid([
|
|
1161
|
+
"field-usage-counter",
|
|
1162
|
+
tenantId,
|
|
1163
|
+
objectRef,
|
|
1164
|
+
fieldName,
|
|
1165
|
+
period
|
|
1166
|
+
]);
|
|
1167
1167
|
}
|
|
1168
|
-
function
|
|
1169
|
-
|
|
1170
|
-
|
|
1168
|
+
async function fieldUsageReportReceiptId(tenantId, userId, objectRef, fieldName, period) {
|
|
1169
|
+
return deterministicFieldsUuid([
|
|
1170
|
+
"field-usage-report-receipt",
|
|
1171
|
+
tenantId,
|
|
1172
|
+
userId,
|
|
1173
|
+
objectRef,
|
|
1174
|
+
fieldName,
|
|
1175
|
+
period
|
|
1176
|
+
]);
|
|
1171
1177
|
}
|
|
1172
|
-
function
|
|
1173
|
-
return
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
displayOrder: row.displayOrder,
|
|
1177
|
-
fieldName: row.fieldName,
|
|
1178
|
-
help: row.help,
|
|
1179
|
-
id: String(row.id),
|
|
1180
|
-
label: row.label,
|
|
1181
|
-
locked: row.locked,
|
|
1182
|
-
scopeType: row.scopeType,
|
|
1183
|
-
tenantId: row.tenantId,
|
|
1184
|
-
updatedBy: row.updatedBy,
|
|
1185
|
-
userId: row.userId,
|
|
1186
|
-
visibility: row.visibility,
|
|
1187
|
-
createdAt: dateText(row.createdAt),
|
|
1188
|
-
updatedAt: dateText(row.updatedAt)
|
|
1189
|
-
};
|
|
1178
|
+
function isUsageAddressableField(field) {
|
|
1179
|
+
if (field._meta?.__smrtSystemField === true) return false;
|
|
1180
|
+
if (field.type === "oneToMany" || field.type === "manyToMany" || field.type === "meta") return false;
|
|
1181
|
+
return !isTransientField(field);
|
|
1190
1182
|
}
|
|
1191
|
-
function
|
|
1192
|
-
return
|
|
1193
|
-
id: String(row.id),
|
|
1194
|
-
objectRef: row.objectRef,
|
|
1195
|
-
fieldName: row.fieldName,
|
|
1196
|
-
scopeType: row.scopeType,
|
|
1197
|
-
tenantId: row.tenantId,
|
|
1198
|
-
userId: row.userId,
|
|
1199
|
-
updatedBy: row.updatedBy,
|
|
1200
|
-
createdAt: dateText(row.createdAt),
|
|
1201
|
-
updatedAt: dateText(row.updatedAt)
|
|
1202
|
-
};
|
|
1183
|
+
function isHistogramEligibleField(field) {
|
|
1184
|
+
return field.type === "boolean" || field.type === "foreignKey" || field.type === "crossPackageRef";
|
|
1203
1185
|
}
|
|
1204
|
-
function
|
|
1205
|
-
|
|
1186
|
+
function serializeHistogramSample(field, value) {
|
|
1187
|
+
if (field.type === "boolean") return typeof value === "boolean" ? String(value) : null;
|
|
1188
|
+
if (isStorableReferenceId(field, value) && value.length <= 64) return value;
|
|
1189
|
+
return null;
|
|
1206
1190
|
}
|
|
1207
|
-
function
|
|
1208
|
-
|
|
1191
|
+
function decodeHistogramKey(field, key) {
|
|
1192
|
+
if (field.type === "boolean") return key === "true";
|
|
1193
|
+
return key;
|
|
1194
|
+
}
|
|
1195
|
+
function validateEntriesInput(rawEntries) {
|
|
1196
|
+
if (!Array.isArray(rawEntries) || rawEntries.length === 0) throw new Error("reportUsage requires a non-empty \"entries\" array of { objectRef, fieldName, value?, matchedDefault? } samples");
|
|
1197
|
+
if (rawEntries.length > 100) throw new Error(`reportUsage accepts at most 100 entries per call (got ${rawEntries.length})`);
|
|
1198
|
+
for (const entry of rawEntries) if (!entry || typeof entry !== "object" || typeof entry.objectRef !== "string" || entry.objectRef.trim() === "" || typeof entry.fieldName !== "string" || entry.fieldName.trim() === "") throw new Error("reportUsage entries must carry non-empty objectRef and fieldName strings");
|
|
1199
|
+
return rawEntries;
|
|
1209
1200
|
}
|
|
1210
1201
|
//#endregion
|
|
1211
1202
|
//#region src/field-policy-resolver.ts
|
|
@@ -1399,7 +1390,7 @@ async function defaultTenantHierarchyLoader(options) {
|
|
|
1399
1390
|
throw error;
|
|
1400
1391
|
}
|
|
1401
1392
|
}
|
|
1402
|
-
var MISSING_MODULE_TARGET_PATTERN = /Cannot find (?:package|module) '([^']+)'/;
|
|
1393
|
+
var MISSING_MODULE_TARGET_PATTERN$1 = /Cannot find (?:package|module) '([^']+)'/;
|
|
1403
1394
|
function isUsersSpecifier(target) {
|
|
1404
1395
|
return target === "@happyvertical/smrt-users" || target.startsWith("@happyvertical/smrt-users/");
|
|
1405
1396
|
}
|
|
@@ -1408,7 +1399,7 @@ function isMissingUsersDependency(error) {
|
|
|
1408
1399
|
const seen = /* @__PURE__ */ new Set();
|
|
1409
1400
|
while (current instanceof Error && !seen.has(current)) {
|
|
1410
1401
|
seen.add(current);
|
|
1411
|
-
const match = current.message.match(MISSING_MODULE_TARGET_PATTERN);
|
|
1402
|
+
const match = current.message.match(MISSING_MODULE_TARGET_PATTERN$1);
|
|
1412
1403
|
if (match && isUsersSpecifier(match[1])) return true;
|
|
1413
1404
|
if (current.message.includes("Failed to load @happyvertical/smrt-users for")) return true;
|
|
1414
1405
|
current = current.cause;
|
|
@@ -1625,9 +1616,506 @@ function spaced(value) {
|
|
|
1625
1616
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ");
|
|
1626
1617
|
}
|
|
1627
1618
|
//#endregion
|
|
1619
|
+
//#region src/usage-learning.ts
|
|
1620
|
+
var __defProp = Object.defineProperty;
|
|
1621
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
1622
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
1623
|
+
enumerable: true,
|
|
1624
|
+
configurable: true,
|
|
1625
|
+
writable: true,
|
|
1626
|
+
value
|
|
1627
|
+
}) : obj[key] = value;
|
|
1628
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
1629
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
1630
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1631
|
+
if (kind && result) __defProp(target, key, result);
|
|
1632
|
+
return result;
|
|
1633
|
+
};
|
|
1634
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1635
|
+
var DAY_MS = 1440 * 60 * 1e3;
|
|
1636
|
+
var FIELD_USAGE_MAINTENANCE_DEFAULTS = {
|
|
1637
|
+
counterMaxAgeMs: 90 * DAY_MS,
|
|
1638
|
+
counterMaxRows: 1e5,
|
|
1639
|
+
suggestionAcceptedMaxAgeMs: 180 * DAY_MS
|
|
1640
|
+
};
|
|
1641
|
+
var FIELD_USAGE_SUGGESTION_DEFAULTS = {
|
|
1642
|
+
windowDays: 30,
|
|
1643
|
+
minDistinctUsers: 5,
|
|
1644
|
+
minSetCount: 10,
|
|
1645
|
+
defaultDominanceRatio: .8
|
|
1646
|
+
};
|
|
1647
|
+
async function pruneFieldUsageCounters(db, retention) {
|
|
1648
|
+
const { maxAgeMs, maxRows } = retention;
|
|
1649
|
+
if (maxAgeMs == null && maxRows == null) throw new Error("pruneFieldUsageCounters requires maxAgeMs and/or maxRows");
|
|
1650
|
+
if (maxAgeMs != null && (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)) throw new Error(`pruneFieldUsageCounters maxAgeMs must be >= 0, got ${maxAgeMs}`);
|
|
1651
|
+
if (maxRows != null && (!Number.isFinite(maxRows) || maxRows < 0)) throw new Error(`pruneFieldUsageCounters maxRows must be >= 0, got ${maxRows}`);
|
|
1652
|
+
const tenantCondition = retention.tenantId ? " AND tenant_id = ?" : "";
|
|
1653
|
+
const tenantParams = retention.tenantId ? [retention.tenantId] : [];
|
|
1654
|
+
let pruned = 0;
|
|
1655
|
+
if (maxAgeMs != null) {
|
|
1656
|
+
const cutoffDay = fieldUsagePeriodForDate(new Date(Date.now() - maxAgeMs));
|
|
1657
|
+
pruned += await deleteCounted(db, `period < ?${tenantCondition}`, [cutoffDay, ...tenantParams]);
|
|
1658
|
+
}
|
|
1659
|
+
if (maxRows != null) {
|
|
1660
|
+
const excess = numberFromRow(getQueryRows(await db.query(`SELECT COUNT(*) AS total FROM _smrt_field_usage_counters${retention.tenantId ? " WHERE tenant_id = ?" : ""}`, ...tenantParams))[0] ?? {}, "total") - Math.floor(maxRows);
|
|
1661
|
+
if (excess > 0) {
|
|
1662
|
+
await db.query(`DELETE FROM _smrt_field_usage_counters
|
|
1663
|
+
WHERE id IN (
|
|
1664
|
+
SELECT id FROM _smrt_field_usage_counters
|
|
1665
|
+
${retention.tenantId ? "WHERE tenant_id = ?" : ""}
|
|
1666
|
+
ORDER BY period ASC, id ASC
|
|
1667
|
+
LIMIT ${excess}
|
|
1668
|
+
)`, ...tenantParams);
|
|
1669
|
+
pruned += excess;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return { pruned };
|
|
1673
|
+
}
|
|
1674
|
+
async function pruneFieldUsageReportReceipts(db, options) {
|
|
1675
|
+
if (!Number.isFinite(options.maxAgeMs) || options.maxAgeMs < 0) throw new Error(`pruneFieldUsageReportReceipts maxAgeMs must be >= 0, got ${options.maxAgeMs}`);
|
|
1676
|
+
const cutoffDay = fieldUsagePeriodForDate(new Date(Date.now() - options.maxAgeMs));
|
|
1677
|
+
const tenantCondition = options.tenantId ? " AND tenant_id = ?" : "";
|
|
1678
|
+
const tenantParams = options.tenantId ? [options.tenantId] : [];
|
|
1679
|
+
return { pruned: await deleteCounted(db, `period < ?${tenantCondition}`, [cutoffDay, ...tenantParams], "_smrt_field_usage_report_receipts") };
|
|
1680
|
+
}
|
|
1681
|
+
async function pruneFieldPolicySuggestions(db, retention) {
|
|
1682
|
+
const { acceptedMaxAgeMs } = retention;
|
|
1683
|
+
if (!Number.isFinite(acceptedMaxAgeMs) || acceptedMaxAgeMs < 0) throw new Error(`pruneFieldPolicySuggestions acceptedMaxAgeMs must be >= 0, got ${acceptedMaxAgeMs}`);
|
|
1684
|
+
const now = retention.now ?? /* @__PURE__ */ new Date();
|
|
1685
|
+
const acceptedCutoff = new Date(now.getTime() - acceptedMaxAgeMs);
|
|
1686
|
+
const tenantCondition = retention.tenantId ? " AND tenant_id = ?" : "";
|
|
1687
|
+
const tenantParams = retention.tenantId ? [retention.tenantId] : [];
|
|
1688
|
+
return { pruned: await deleteCounted(db, `((status = 'dismissed' AND cooldown_until IS NOT NULL AND cooldown_until <= ?) OR (status = 'accepted' AND decided_at IS NOT NULL AND decided_at <= ?))${tenantCondition}`, [
|
|
1689
|
+
now.toISOString(),
|
|
1690
|
+
acceptedCutoff.toISOString(),
|
|
1691
|
+
...tenantParams
|
|
1692
|
+
], "_smrt_field_policy_suggestions") };
|
|
1693
|
+
}
|
|
1694
|
+
async function runFieldUsageMaintenance(options) {
|
|
1695
|
+
const config = normalizeMaintenanceConfig(options);
|
|
1696
|
+
const ambientTenantId = restrictingTenantId();
|
|
1697
|
+
const counters = await pruneFieldUsageCounters(options.db, {
|
|
1698
|
+
maxAgeMs: config.counterMaxAgeMs,
|
|
1699
|
+
maxRows: config.counterMaxRows,
|
|
1700
|
+
tenantId: ambientTenantId
|
|
1701
|
+
});
|
|
1702
|
+
const receipts = await pruneFieldUsageReportReceipts(options.db, {
|
|
1703
|
+
maxAgeMs: config.counterMaxAgeMs,
|
|
1704
|
+
tenantId: ambientTenantId
|
|
1705
|
+
});
|
|
1706
|
+
const suggestions = await pruneFieldPolicySuggestions(options.db, {
|
|
1707
|
+
acceptedMaxAgeMs: config.suggestionAcceptedMaxAgeMs,
|
|
1708
|
+
tenantId: ambientTenantId
|
|
1709
|
+
});
|
|
1710
|
+
return {
|
|
1711
|
+
countersPruned: counters.pruned,
|
|
1712
|
+
receiptsPruned: receipts.pruned,
|
|
1713
|
+
suggestionsPruned: suggestions.pruned
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
async function runFieldPolicySuggestionGeneration(options) {
|
|
1717
|
+
const config = normalizeSuggestionConfig(options);
|
|
1718
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1719
|
+
const ambientTenantId = restrictingTenantId();
|
|
1720
|
+
const counters = await FieldUsageCounterCollection.create({ db: options.db });
|
|
1721
|
+
const suggestions = await FieldPolicySuggestionCollection.create({ db: options.db });
|
|
1722
|
+
const fromPeriod = fieldUsagePeriodForDate(/* @__PURE__ */ new Date(now.getTime() - (config.windowDays - 1) * DAY_MS));
|
|
1723
|
+
const toPeriod = fieldUsagePeriodForDate(now);
|
|
1724
|
+
const groups = groupCounters(await counters.listWindow({
|
|
1725
|
+
fromPeriod,
|
|
1726
|
+
toPeriod,
|
|
1727
|
+
tenantId: ambientTenantId
|
|
1728
|
+
}));
|
|
1729
|
+
const fieldMaps = /* @__PURE__ */ new Map();
|
|
1730
|
+
const resolvedPolicies = /* @__PURE__ */ new Map();
|
|
1731
|
+
const summary = {
|
|
1732
|
+
created: 0,
|
|
1733
|
+
suppressed: 0,
|
|
1734
|
+
groupsConsidered: 0,
|
|
1735
|
+
groupsFailed: 0,
|
|
1736
|
+
failures: []
|
|
1737
|
+
};
|
|
1738
|
+
for (const group of groups) {
|
|
1739
|
+
summary.groupsConsidered += 1;
|
|
1740
|
+
try {
|
|
1741
|
+
await evaluateGroup(group);
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
summary.groupsFailed += 1;
|
|
1744
|
+
if (summary.failures.length < 5) summary.failures.push(`${group.tenantId} ${group.objectRef}.${group.fieldName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return summary;
|
|
1748
|
+
async function evaluateGroup(group) {
|
|
1749
|
+
let fieldMap = fieldMaps.get(group.objectRef);
|
|
1750
|
+
if (fieldMap === void 0) {
|
|
1751
|
+
try {
|
|
1752
|
+
fieldMap = await getObjectFieldMap(group.objectRef);
|
|
1753
|
+
} catch {
|
|
1754
|
+
fieldMap = null;
|
|
1755
|
+
}
|
|
1756
|
+
fieldMaps.set(group.objectRef, fieldMap);
|
|
1757
|
+
}
|
|
1758
|
+
if (!fieldMap) return;
|
|
1759
|
+
const fieldDef = fieldMap.get(group.fieldName);
|
|
1760
|
+
if (!fieldDef || isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) return;
|
|
1761
|
+
const stats = mergeGroupStats(group.buckets);
|
|
1762
|
+
const policyKey = `${group.tenantId}\0${group.objectRef}`;
|
|
1763
|
+
let orgFields = resolvedPolicies.get(policyKey);
|
|
1764
|
+
if (!orgFields) {
|
|
1765
|
+
const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
1766
|
+
orgFields = (await resolveFieldPolicy(group.objectRef, {
|
|
1767
|
+
tenantId: group.tenantId,
|
|
1768
|
+
db: options.db
|
|
1769
|
+
})).fields;
|
|
1770
|
+
resolvedPolicies.set(policyKey, orgFields);
|
|
1771
|
+
}
|
|
1772
|
+
const fieldPolicy = orgFields[group.fieldName];
|
|
1773
|
+
if (!fieldPolicy) return;
|
|
1774
|
+
if (stats.distinctUsers >= config.minDistinctUsers && fieldPolicy.visibility !== "basic") if (await createUnlessSuppressed(suggestions, {
|
|
1775
|
+
tenantId: group.tenantId,
|
|
1776
|
+
objectRef: group.objectRef,
|
|
1777
|
+
fieldName: group.fieldName,
|
|
1778
|
+
kind: "promote",
|
|
1779
|
+
proposedValue: null,
|
|
1780
|
+
evidence: buildFieldUsageEvidence({
|
|
1781
|
+
kind: "promote",
|
|
1782
|
+
fieldName: group.fieldName,
|
|
1783
|
+
objectRef: group.objectRef,
|
|
1784
|
+
windowStart: fromPeriod,
|
|
1785
|
+
windowEnd: toPeriod,
|
|
1786
|
+
distinctUsers: stats.distinctUsers,
|
|
1787
|
+
distinctUsersAtLeast: stats.distinctUsersOverflowed,
|
|
1788
|
+
setCount: stats.setCount,
|
|
1789
|
+
submissionCount: stats.submissionTotalKnown ? stats.submissionCount : void 0,
|
|
1790
|
+
threshold: config.minDistinctUsers
|
|
1791
|
+
}),
|
|
1792
|
+
now
|
|
1793
|
+
})) summary.created += 1;
|
|
1794
|
+
else summary.suppressed += 1;
|
|
1795
|
+
if (isHistogramEligibleField(fieldDef) && stats.submissionTotalKnown && stats.submissionCount >= config.minSetCount) {
|
|
1796
|
+
const top = topHistogramEntry(stats.histogram);
|
|
1797
|
+
if (top) {
|
|
1798
|
+
const share = top.count / stats.submissionCount;
|
|
1799
|
+
const proposed = decodeHistogramKey(fieldDef, top.key);
|
|
1800
|
+
const currentDefault = fieldPolicy.hasDefault ? fieldPolicy.defaultValue : void 0;
|
|
1801
|
+
if (share >= config.defaultDominanceRatio && !sameProposedValue(proposed, currentDefault)) if (await createUnlessSuppressed(suggestions, {
|
|
1802
|
+
tenantId: group.tenantId,
|
|
1803
|
+
objectRef: group.objectRef,
|
|
1804
|
+
fieldName: group.fieldName,
|
|
1805
|
+
kind: "default",
|
|
1806
|
+
proposedValue: JSON.stringify(proposed),
|
|
1807
|
+
evidence: buildFieldUsageEvidence({
|
|
1808
|
+
kind: "default",
|
|
1809
|
+
fieldName: group.fieldName,
|
|
1810
|
+
objectRef: group.objectRef,
|
|
1811
|
+
windowStart: fromPeriod,
|
|
1812
|
+
windowEnd: toPeriod,
|
|
1813
|
+
distinctUsers: stats.distinctUsers,
|
|
1814
|
+
distinctUsersAtLeast: stats.distinctUsersOverflowed,
|
|
1815
|
+
setCount: stats.setCount,
|
|
1816
|
+
submissionCount: stats.submissionCount,
|
|
1817
|
+
threshold: config.minSetCount,
|
|
1818
|
+
topValue: proposed,
|
|
1819
|
+
topValueShare: share
|
|
1820
|
+
}),
|
|
1821
|
+
now
|
|
1822
|
+
})) summary.created += 1;
|
|
1823
|
+
else summary.suppressed += 1;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
var FieldUsageLearningAgent = class extends SmrtObject {
|
|
1829
|
+
constructor(options = {}) {
|
|
1830
|
+
super(options);
|
|
1831
|
+
}
|
|
1832
|
+
/** Schedule entry point for {@link runFieldUsageMaintenance}. */
|
|
1833
|
+
async runUsageMaintenance(args = {}) {
|
|
1834
|
+
return runFieldUsageMaintenance({
|
|
1835
|
+
db: this.db,
|
|
1836
|
+
...pickFiniteNumbers(args, [
|
|
1837
|
+
"counterMaxAgeMs",
|
|
1838
|
+
"counterMaxRows",
|
|
1839
|
+
"suggestionAcceptedMaxAgeMs"
|
|
1840
|
+
])
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
/** Schedule entry point for {@link runFieldPolicySuggestionGeneration}. */
|
|
1844
|
+
async runSuggestionGeneration(args = {}) {
|
|
1845
|
+
return runFieldPolicySuggestionGeneration({
|
|
1846
|
+
db: this.db,
|
|
1847
|
+
...pickFiniteNumbers(args, [
|
|
1848
|
+
"windowDays",
|
|
1849
|
+
"minDistinctUsers",
|
|
1850
|
+
"minSetCount",
|
|
1851
|
+
"defaultDominanceRatio"
|
|
1852
|
+
])
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
/**
|
|
1857
|
+
* The smrt-jobs opt-in background allowlist (S5 contract): ONLY these two
|
|
1858
|
+
* methods are reachable from a persisted job/schedule row.
|
|
1859
|
+
*/
|
|
1860
|
+
__publicField(FieldUsageLearningAgent, "backgroundEligibleMethods", ["runUsageMaintenance", "runSuggestionGeneration"]);
|
|
1861
|
+
FieldUsageLearningAgent = __decorateClass([smrt({
|
|
1862
|
+
tableName: "_smrt_field_usage_learning_agents",
|
|
1863
|
+
api: { include: [] },
|
|
1864
|
+
cli: false,
|
|
1865
|
+
mcp: { include: [] }
|
|
1866
|
+
})], FieldUsageLearningAgent);
|
|
1867
|
+
function buildFieldUsageEvidence(input) {
|
|
1868
|
+
const users = `${input.distinctUsersAtLeast ? "at least " : ""}${input.distinctUsers} user${input.distinctUsers === 1 && !input.distinctUsersAtLeast ? "" : "s"}`;
|
|
1869
|
+
const deviations = `${input.setCount} of ` + (input.submissionCount === void 0 ? "an unrecorded number of submissions" : `${input.submissionCount} submission${input.submissionCount === 1 ? "" : "s"}`);
|
|
1870
|
+
const base = `${users} set "${input.fieldName}" to a non-default value in ${deviations} between ${input.windowStart} and ${input.windowEnd}.`;
|
|
1871
|
+
return {
|
|
1872
|
+
summary: input.kind === "default" ? `${base} ${Math.round((input.topValueShare ?? 0) * 100)}% of all ${input.submissionCount ?? 0} submissions used the value ${JSON.stringify(input.topValue)}.` : base,
|
|
1873
|
+
objectRef: input.objectRef,
|
|
1874
|
+
fieldName: input.fieldName,
|
|
1875
|
+
windowStart: input.windowStart,
|
|
1876
|
+
windowEnd: input.windowEnd,
|
|
1877
|
+
distinctUsers: input.distinctUsers,
|
|
1878
|
+
...input.distinctUsersAtLeast ? { distinctUsersAtLeast: true } : {},
|
|
1879
|
+
setCount: input.setCount,
|
|
1880
|
+
...input.submissionCount !== void 0 ? { submissionCount: input.submissionCount } : { submissionCountUnknown: true },
|
|
1881
|
+
threshold: input.threshold,
|
|
1882
|
+
...input.topValue !== void 0 ? { topValue: input.topValue } : {},
|
|
1883
|
+
...input.topValueShare !== void 0 ? { topValueShare: Number(input.topValueShare.toFixed(4)) } : {}
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function groupCounters(rows) {
|
|
1887
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1888
|
+
for (const row of rows) {
|
|
1889
|
+
if (!row.tenantId) continue;
|
|
1890
|
+
const key = `${row.tenantId}\0${row.objectRef}\0${row.fieldName}`;
|
|
1891
|
+
let group = byKey.get(key);
|
|
1892
|
+
if (!group) {
|
|
1893
|
+
group = {
|
|
1894
|
+
tenantId: row.tenantId,
|
|
1895
|
+
objectRef: row.objectRef,
|
|
1896
|
+
fieldName: row.fieldName,
|
|
1897
|
+
buckets: []
|
|
1898
|
+
};
|
|
1899
|
+
byKey.set(key, group);
|
|
1900
|
+
}
|
|
1901
|
+
group.buckets.push(row);
|
|
1902
|
+
}
|
|
1903
|
+
return [...byKey.values()];
|
|
1904
|
+
}
|
|
1905
|
+
function mergeGroupStats(buckets) {
|
|
1906
|
+
let submissionCount = 0;
|
|
1907
|
+
let submissionTotalKnown = true;
|
|
1908
|
+
let setCount = 0;
|
|
1909
|
+
let overflowed = false;
|
|
1910
|
+
const users = /* @__PURE__ */ new Set();
|
|
1911
|
+
const histogram = emptyHistogram();
|
|
1912
|
+
for (const bucket of buckets) {
|
|
1913
|
+
submissionCount += bucket.submissionCount;
|
|
1914
|
+
if (bucket.isLegacyBucket()) submissionTotalKnown = false;
|
|
1915
|
+
setCount += bucket.setCount;
|
|
1916
|
+
overflowed = overflowed || bucket.distinctUsersOverflowed;
|
|
1917
|
+
for (const id of bucket.getDistinctUserIds()) users.add(id);
|
|
1918
|
+
for (const [key, count] of Object.entries(bucket.getValueHistogram())) histogram[key] = (Object.hasOwn(histogram, key) ? histogram[key] : 0) + count;
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1921
|
+
submissionCount,
|
|
1922
|
+
submissionTotalKnown,
|
|
1923
|
+
setCount,
|
|
1924
|
+
distinctUsers: users.size,
|
|
1925
|
+
distinctUsersOverflowed: overflowed,
|
|
1926
|
+
histogram
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function topHistogramEntry(histogram) {
|
|
1930
|
+
let top = null;
|
|
1931
|
+
for (const [key, count] of Object.entries(histogram)) if (!top || count > top.count) top = {
|
|
1932
|
+
key,
|
|
1933
|
+
count
|
|
1934
|
+
};
|
|
1935
|
+
return top;
|
|
1936
|
+
}
|
|
1937
|
+
function sameProposedValue(a, b) {
|
|
1938
|
+
if (a === b) return true;
|
|
1939
|
+
try {
|
|
1940
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1941
|
+
} catch {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
async function createUnlessSuppressed(suggestions, candidate) {
|
|
1946
|
+
if ((await suggestions.list({ where: {
|
|
1947
|
+
tenantId: candidate.tenantId,
|
|
1948
|
+
objectRef: candidate.objectRef,
|
|
1949
|
+
fieldName: candidate.fieldName,
|
|
1950
|
+
kind: candidate.kind
|
|
1951
|
+
} })).some((row) => row.activeKey === "active" || row.status === "dismissed" && isWithinCoolDown(row, candidate.now))) return false;
|
|
1952
|
+
await suggestions.create({
|
|
1953
|
+
tenantId: candidate.tenantId,
|
|
1954
|
+
objectRef: candidate.objectRef,
|
|
1955
|
+
fieldName: candidate.fieldName,
|
|
1956
|
+
kind: candidate.kind,
|
|
1957
|
+
proposedValue: candidate.proposedValue,
|
|
1958
|
+
evidence: JSON.stringify(candidate.evidence),
|
|
1959
|
+
status: "pending"
|
|
1960
|
+
});
|
|
1961
|
+
return true;
|
|
1962
|
+
}
|
|
1963
|
+
function isWithinCoolDown(row, now) {
|
|
1964
|
+
const raw = row.cooldownUntil;
|
|
1965
|
+
if (raw === null || raw === void 0) return false;
|
|
1966
|
+
const until = raw instanceof Date ? raw.getTime() : Date.parse(String(raw));
|
|
1967
|
+
return Number.isFinite(until) && until > now.getTime();
|
|
1968
|
+
}
|
|
1969
|
+
function restrictingTenantId() {
|
|
1970
|
+
const context = getCurrentTenant();
|
|
1971
|
+
if (!context || isSuperAdminBypass()) return null;
|
|
1972
|
+
return context.tenantId;
|
|
1973
|
+
}
|
|
1974
|
+
function normalizeMaintenanceConfig(options) {
|
|
1975
|
+
return {
|
|
1976
|
+
counterMaxAgeMs: nonNegativeOrDefault(options.counterMaxAgeMs, FIELD_USAGE_MAINTENANCE_DEFAULTS.counterMaxAgeMs),
|
|
1977
|
+
counterMaxRows: nonNegativeOrDefault(options.counterMaxRows, FIELD_USAGE_MAINTENANCE_DEFAULTS.counterMaxRows),
|
|
1978
|
+
suggestionAcceptedMaxAgeMs: nonNegativeOrDefault(options.suggestionAcceptedMaxAgeMs, FIELD_USAGE_MAINTENANCE_DEFAULTS.suggestionAcceptedMaxAgeMs)
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function normalizeSuggestionConfig(options) {
|
|
1982
|
+
const ratio = positiveOrDefault(options.defaultDominanceRatio, FIELD_USAGE_SUGGESTION_DEFAULTS.defaultDominanceRatio);
|
|
1983
|
+
return {
|
|
1984
|
+
windowDays: Math.max(1, Math.floor(positiveOrDefault(options.windowDays, FIELD_USAGE_SUGGESTION_DEFAULTS.windowDays))),
|
|
1985
|
+
minDistinctUsers: Math.max(1, Math.floor(positiveOrDefault(options.minDistinctUsers, FIELD_USAGE_SUGGESTION_DEFAULTS.minDistinctUsers))),
|
|
1986
|
+
minSetCount: Math.max(1, Math.floor(positiveOrDefault(options.minSetCount, FIELD_USAGE_SUGGESTION_DEFAULTS.minSetCount))),
|
|
1987
|
+
defaultDominanceRatio: Math.min(Math.max(ratio, .01), 1)
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function positiveOrDefault(value, fallback) {
|
|
1991
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1992
|
+
}
|
|
1993
|
+
function nonNegativeOrDefault(value, fallback) {
|
|
1994
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1995
|
+
}
|
|
1996
|
+
function pickFiniteNumbers(args, keys) {
|
|
1997
|
+
const picked = {};
|
|
1998
|
+
if (!args || typeof args !== "object") return picked;
|
|
1999
|
+
for (const key of keys) {
|
|
2000
|
+
const value = args[key];
|
|
2001
|
+
if (typeof value === "number" && Number.isFinite(value)) picked[key] = value;
|
|
2002
|
+
}
|
|
2003
|
+
return picked;
|
|
2004
|
+
}
|
|
2005
|
+
async function deleteCounted(db, condition, params, table = "_smrt_field_usage_counters") {
|
|
2006
|
+
const total = numberFromRow(getQueryRows(await db.query(`SELECT COUNT(*) AS total FROM ${table} WHERE ${condition}`, ...params))[0] ?? {}, "total");
|
|
2007
|
+
if (total > 0) await db.query(`DELETE FROM ${table} WHERE ${condition}`, ...params);
|
|
2008
|
+
return total;
|
|
2009
|
+
}
|
|
2010
|
+
function getQueryRows(result) {
|
|
2011
|
+
return Array.isArray(result) ? result : result?.rows ?? [];
|
|
2012
|
+
}
|
|
2013
|
+
function numberFromRow(row, key) {
|
|
2014
|
+
const value = row[key];
|
|
2015
|
+
if (typeof value === "number") return value;
|
|
2016
|
+
if (typeof value === "bigint") return Number(value);
|
|
2017
|
+
if (typeof value === "string") return Number.parseFloat(value) || 0;
|
|
2018
|
+
return 0;
|
|
2019
|
+
}
|
|
2020
|
+
//#endregion
|
|
2021
|
+
//#region src/users-module.ts
|
|
2022
|
+
var MISSING_MODULE_TARGET_PATTERN = /Cannot find (?:package|module) '([^']+)'/;
|
|
2023
|
+
function isPackageSpecifier(target, packageName) {
|
|
2024
|
+
return target === packageName || target.startsWith(`${packageName}/`);
|
|
2025
|
+
}
|
|
2026
|
+
function isMissingWorkspaceDependency(error, packageName) {
|
|
2027
|
+
let current = error;
|
|
2028
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2029
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
2030
|
+
seen.add(current);
|
|
2031
|
+
const match = current.message.match(MISSING_MODULE_TARGET_PATTERN);
|
|
2032
|
+
if (match && isPackageSpecifier(match[1], packageName)) return true;
|
|
2033
|
+
if (current.message.includes(`Failed to load ${packageName} for`)) return true;
|
|
2034
|
+
current = current.cause;
|
|
2035
|
+
}
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
//#endregion
|
|
2039
|
+
//#region src/usage-schedules.ts
|
|
2040
|
+
var FIELD_USAGE_LEARNING_AGENT_TYPE = "@happyvertical/smrt-fields:FieldUsageLearningAgent";
|
|
2041
|
+
var FIELD_USAGE_MAINTENANCE_METHOD = "runUsageMaintenance";
|
|
2042
|
+
var FIELD_USAGE_SUGGESTION_METHOD = "runSuggestionGeneration";
|
|
2043
|
+
function fieldUsageScheduleId(method) {
|
|
2044
|
+
return deterministicFieldsUuid([
|
|
2045
|
+
"field-usage-learning-schedule",
|
|
2046
|
+
FIELD_USAGE_LEARNING_AGENT_TYPE,
|
|
2047
|
+
method
|
|
2048
|
+
]);
|
|
2049
|
+
}
|
|
2050
|
+
async function ensureFieldUsageLearningSchedules(options) {
|
|
2051
|
+
const context = getCurrentTenant();
|
|
2052
|
+
if (context && !isSuperAdminBypass()) throw new TenantIsolationError("ensureFieldUsageLearningSchedules installs GLOBAL schedules and must run from trusted execution (no ambient tenant context, or super-admin bypass)", { tenantId: context.tenantId });
|
|
2053
|
+
let agentsModule = options.agentsModule;
|
|
2054
|
+
if (!agentsModule) try {
|
|
2055
|
+
agentsModule = await importWorkspaceModule({
|
|
2056
|
+
packageName: "@happyvertical/smrt-agents",
|
|
2057
|
+
sourceEntry: "packages/agents/src/index.ts",
|
|
2058
|
+
purpose: "field usage learning schedule installation"
|
|
2059
|
+
});
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
if (isMissingWorkspaceDependency(error, "@happyvertical/smrt-agents")) return {
|
|
2062
|
+
installed: false,
|
|
2063
|
+
created: 0
|
|
2064
|
+
};
|
|
2065
|
+
throw error;
|
|
2066
|
+
}
|
|
2067
|
+
const schedules = await agentsModule.AgentScheduleCollection.create({ db: options.db });
|
|
2068
|
+
const enabled = options.enabled ?? false;
|
|
2069
|
+
let created = 0;
|
|
2070
|
+
const definitions = [{
|
|
2071
|
+
method: FIELD_USAGE_MAINTENANCE_METHOD,
|
|
2072
|
+
cron: options.maintenanceCron ?? "30 2 * * *",
|
|
2073
|
+
methodArgs: options.maintenanceArgs ?? {}
|
|
2074
|
+
}, {
|
|
2075
|
+
method: FIELD_USAGE_SUGGESTION_METHOD,
|
|
2076
|
+
cron: options.suggestionCron ?? "0 3 * * 1",
|
|
2077
|
+
methodArgs: options.suggestionArgs ?? {}
|
|
2078
|
+
}];
|
|
2079
|
+
for (const definition of definitions) {
|
|
2080
|
+
if ((await schedules.list({ where: {
|
|
2081
|
+
agentType: "@happyvertical/smrt-fields:FieldUsageLearningAgent",
|
|
2082
|
+
method: definition.method
|
|
2083
|
+
} })).some(isGlobalScheduleRow)) continue;
|
|
2084
|
+
const id = await fieldUsageScheduleId(definition.method);
|
|
2085
|
+
let row;
|
|
2086
|
+
try {
|
|
2087
|
+
row = await schedules.create({
|
|
2088
|
+
id,
|
|
2089
|
+
tenantId: null,
|
|
2090
|
+
agentType: FIELD_USAGE_LEARNING_AGENT_TYPE,
|
|
2091
|
+
agentId: null,
|
|
2092
|
+
cron: definition.cron,
|
|
2093
|
+
method: definition.method,
|
|
2094
|
+
agentConfig: {},
|
|
2095
|
+
methodArgs: definition.methodArgs,
|
|
2096
|
+
enabled,
|
|
2097
|
+
status: enabled ? "active" : "disabled",
|
|
2098
|
+
_insertOnly: true
|
|
2099
|
+
});
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
if ((await schedules.list({ where: { id } })).length === 0) throw error;
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
await row.save?.();
|
|
2105
|
+
created += 1;
|
|
2106
|
+
}
|
|
2107
|
+
return {
|
|
2108
|
+
installed: true,
|
|
2109
|
+
created
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function isGlobalScheduleRow(row) {
|
|
2113
|
+
return row.tenantId === null || row.tenantId === void 0;
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
1628
2116
|
//#region src/index.ts
|
|
1629
2117
|
ensureFieldPolicyPermissionsRegistered();
|
|
1630
2118
|
//#endregion
|
|
1631
|
-
export { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_PERMISSION_DEFINITIONS, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES, FieldPolicy, FieldPolicyCollection, MANAGE_FIELD_POLICY_PERMISSION, PERSONALIZE_FIELD_POLICY_PERMISSION, assertDefaultValueMatchesFieldType, buildCodeSeedDelta, buildCodeSeedVisibility, buildFieldPolicySettingsCatalog, clearFieldPolicyCache, ensureFieldPolicyPermissionsRegistered, fieldPolicyCatalogItemId, getCodeDefault, getCodeSeedGroup, getFieldPolicyCacheTtlMs, getFieldReadPermission, getObjectFieldMap, invalidateFieldPolicyCache, isPolicyAddressableField, isRequiredField, isSensitiveField, isTransientField, isUsableRequiredDefault, parseFieldPolicyCatalogQuery, requireRegisteredObject, resolveFieldPolicy, resolveFieldPolicyExplained, resolveSurvivingTenantChainIds, sanitizeFieldUIHints };
|
|
2119
|
+
export { ACTIVE_SUGGESTION_KEY, APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_PERMISSION_DEFINITIONS, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES, FIELD_USAGE_LEARNING_AGENT_TYPE, FieldPolicy, FieldPolicyCollection, FieldPolicySuggestion, FieldPolicySuggestionCollection, FieldPolicySuggestionConflictError, FieldUsageCounter, FieldUsageCounterCollection, FieldUsageLearningAgent, MANAGE_FIELD_POLICY_PERMISSION, MAX_DISTINCT_USERS_PER_BUCKET, MAX_USAGE_REPORT_ENTRIES, MAX_VALUE_HISTOGRAM_BUCKETS, PERSONALIZE_FIELD_POLICY_PERMISSION, assertDefaultValueMatchesFieldType, buildCodeSeedDelta, buildCodeSeedVisibility, buildFieldPolicySettingsCatalog, clearFieldPolicyCache, decodeHistogramKey, ensureFieldPolicyPermissionsRegistered, ensureFieldUsageLearningSchedules, fieldPolicyCatalogItemId, fieldUsagePeriodForDate, getCodeDefault, getCodeSeedGroup, getFieldPolicyCacheTtlMs, getFieldReadPermission, getObjectFieldMap, invalidateFieldPolicyCache, isHistogramEligibleField, isPolicyAddressableField, isRequiredField, isSensitiveField, isStorableReferenceId, isTransientField, isUsableRequiredDefault, parseFieldPolicyCatalogQuery, pruneFieldPolicySuggestions, pruneFieldUsageCounters, requireRegisteredObject, resolveFieldPolicy, resolveFieldPolicyExplained, resolveSurvivingTenantChainIds, runFieldPolicySuggestionGeneration, runFieldUsageMaintenance, sanitizeFieldUIHints, serializeHistogramSample, field_policy_resolver_exports as t };
|
|
1632
2120
|
|
|
1633
2121
|
//# sourceMappingURL=index.js.map
|