@happyvertical/smrt-fields 0.40.60 → 0.40.62
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 +58 -12
- package/README.md +143 -42
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/cache.d.ts +18 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js +1218 -0
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js.map +1 -0
- package/dist/collections/FieldPolicyCollection.d.ts +95 -0
- package/dist/collections/FieldPolicyCollection.d.ts.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 +85 -0
- package/dist/field-definitions.d.ts.map +1 -0
- package/dist/field-policy-resolver.d.ts +46 -0
- package/dist/field-policy-resolver.d.ts.map +1 -0
- package/dist/index.d.ts +15 -532
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1813 -628
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1487 -191
- package/dist/models/FieldPolicy.d.ts +163 -0
- package/dist/models/FieldPolicy.d.ts.map +1 -0
- 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/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +39 -0
- package/dist/playground.js.map +1 -0
- package/dist/settings-catalog.d.ts +73 -0
- package/dist/settings-catalog.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +449 -9
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +277 -0
- package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
- package/dist/svelte/__tests__/FormHelp.test.js +138 -0
- package/dist/svelte/__tests__/ModeUX.test.js +109 -0
- package/dist/svelte/__tests__/ObjectForm.test.js +687 -0
- package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
- package/dist/svelte/__tests__/PolicyField.test.js +373 -0
- package/dist/svelte/__tests__/UsageLearning.test.js +140 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +37 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +15 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
- 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__/fixtures/ToggleModeButton.svelte +14 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
- package/dist/svelte/components/AdvancedFields.svelte +49 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldInput.svelte +159 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte +288 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +29 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte +24 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte +169 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +20 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
- 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 +157 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ModeSwitch.svelte +47 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectForm.svelte +440 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts +50 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PolicyField.svelte +338 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.d.ts +54 -0
- package/dist/svelte/context.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.js +55 -0
- package/dist/svelte/data-table.d.ts +17 -0
- package/dist/svelte/data-table.d.ts.map +1 -0
- package/dist/svelte/data-table.js +19 -0
- package/dist/svelte/field-policy-editor.d.ts +81 -0
- package/dist/svelte/field-policy-editor.d.ts.map +1 -0
- package/dist/svelte/field-policy-editor.js +120 -0
- package/dist/svelte/gear-context.svelte.d.ts +16 -0
- package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
- package/dist/svelte/gear-context.svelte.js +15 -0
- package/dist/svelte/index.d.ts +63 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +41 -0
- package/dist/svelte/input-registry.d.ts +21 -0
- package/dist/svelte/input-registry.d.ts.map +1 -0
- package/dist/svelte/input-registry.js +26 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source-context.svelte.js +8 -0
- package/dist/svelte/object-form-source.svelte.d.ts +18 -0
- package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source.svelte.js +99 -0
- package/dist/svelte/object-form.d.ts +6 -0
- package/dist/svelte/object-form.d.ts.map +1 -0
- package/dist/svelte/object-form.js +38 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +20 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +38 -0
- package/dist/svelte/settings-catalog.d.ts +54 -0
- package/dist/svelte/settings-catalog.d.ts.map +1 -0
- package/dist/svelte/settings-catalog.js +116 -0
- 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/types.d.ts +101 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +4 -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 +316 -203
- package/dist/types.d.ts.map +1 -0
- 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 +33 -8
package/dist/index.js
CHANGED
|
@@ -1,737 +1,1210 @@
|
|
|
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";
|
|
4
|
+
import { assertOperationPermission } from "@happyvertical/smrt-users";
|
|
3
5
|
import { TenantIsolationError, assertTenantReadAllowed, getCurrentTenant, isSuperAdminBypass, tenantId } from "@happyvertical/smrt-tenancy";
|
|
4
6
|
import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
|
|
5
|
-
//#region \0rolldown/runtime.js
|
|
6
|
-
var __defProp$2 = Object.defineProperty;
|
|
7
|
-
var __exportAll = (all, no_symbols) => {
|
|
8
|
-
let target = {};
|
|
9
|
-
for (var name in all) __defProp$2(target, name, {
|
|
10
|
-
get: all[name],
|
|
11
|
-
enumerable: true
|
|
12
|
-
});
|
|
13
|
-
if (!no_symbols) __defProp$2(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
-
return target;
|
|
15
|
-
};
|
|
16
|
-
//#endregion
|
|
17
7
|
//#region src/__smrt-register__.ts
|
|
18
|
-
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.60\",\"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\":{\"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}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\",\"resolveBatch\"],\"routes\":{\"resolveBatch\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"resolve\"}}},\"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\"]},\"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\"]}"));
|
|
8
|
+
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.62\",\"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\"]}"));
|
|
19
9
|
//#endregion
|
|
20
|
-
//#region src/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return "db-instance:unknown";
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
return `db-config:${JSON.stringify(dbObject)}`;
|
|
38
|
-
} catch {
|
|
39
|
-
return "db-config:opaque";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return "db:unknown";
|
|
43
|
-
}
|
|
44
|
-
var hierarchyLoaderIds = /* @__PURE__ */ new WeakMap();
|
|
45
|
-
var nextLoaderId = 1;
|
|
46
|
-
function getHierarchyLoaderNamespace(loader) {
|
|
47
|
-
if (typeof loader !== "function") return "loader:default";
|
|
48
|
-
const key = loader;
|
|
49
|
-
if (!hierarchyLoaderIds.has(key)) hierarchyLoaderIds.set(key, `loader:${nextLoaderId++}`);
|
|
50
|
-
return hierarchyLoaderIds.get(key) ?? "loader:unknown";
|
|
51
|
-
}
|
|
52
|
-
function buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
53
|
-
return `${getDbNamespace(db)}::${objectRef}::${tenantId ?? "app"}::${userId ?? "user:none"}::${getHierarchyLoaderNamespace(hierarchyLoader)}`;
|
|
54
|
-
}
|
|
55
|
-
function getFieldPolicyCacheTtlMs() {
|
|
56
|
-
return FIELD_POLICY_CACHE_TTL_MS;
|
|
57
|
-
}
|
|
58
|
-
function getCachedFieldPolicy(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
59
|
-
const cacheKey = buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader);
|
|
60
|
-
const cached = fieldPolicyCache.get(cacheKey);
|
|
61
|
-
if (!cached) return null;
|
|
62
|
-
if (cached.expiresAt <= Date.now()) {
|
|
63
|
-
fieldPolicyCache.delete(cacheKey);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
return cached.value;
|
|
67
|
-
}
|
|
68
|
-
function setCachedFieldPolicy(objectRef, tenantId, userId, db, value, hierarchyLoader) {
|
|
69
|
-
fieldPolicyCache.set(buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader), {
|
|
70
|
-
expiresAt: Date.now() + FIELD_POLICY_CACHE_TTL_MS,
|
|
71
|
-
value
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
function invalidateFieldPolicyCache(objectRef, db) {
|
|
75
|
-
const keyPrefix = `${getDbNamespace(db)}::${objectRef}::`;
|
|
76
|
-
for (const cacheKey of fieldPolicyCache.keys()) if (cacheKey.startsWith(keyPrefix)) fieldPolicyCache.delete(cacheKey);
|
|
77
|
-
}
|
|
78
|
-
function clearFieldPolicyCache() {
|
|
79
|
-
fieldPolicyCache.clear();
|
|
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("-");
|
|
80
24
|
}
|
|
81
25
|
//#endregion
|
|
82
|
-
//#region src/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
async function getObjectFieldMap(objectRef) {
|
|
88
|
-
requireRegisteredObject(objectRef);
|
|
89
|
-
return ObjectRegistry.getAllFields(objectRef);
|
|
90
|
-
}
|
|
91
|
-
function sanitizeFieldUIHints(value) {
|
|
92
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
93
|
-
const raw = value;
|
|
94
|
-
const ui = {
|
|
95
|
-
...typeof raw.basic === "boolean" ? { basic: raw.basic } : {},
|
|
96
|
-
...typeof raw.group === "string" ? { group: raw.group } : {},
|
|
97
|
-
...typeof raw.order === "number" && Number.isFinite(raw.order) ? { order: raw.order } : {},
|
|
98
|
-
...typeof raw.locked === "boolean" ? { locked: raw.locked } : {}
|
|
99
|
-
};
|
|
100
|
-
return Object.keys(ui).length > 0 ? ui : void 0;
|
|
101
|
-
}
|
|
102
|
-
function isSensitiveField(field) {
|
|
103
|
-
return field.sensitive === true || field._meta?.sensitive === true;
|
|
104
|
-
}
|
|
105
|
-
function getFieldReadPermission(field) {
|
|
106
|
-
if (typeof field.readPermission === "string") return field.readPermission;
|
|
107
|
-
const metaPermission = field._meta?.readPermission;
|
|
108
|
-
return typeof metaPermission === "string" ? metaPermission : void 0;
|
|
109
|
-
}
|
|
110
|
-
function isTransientField(field) {
|
|
111
|
-
return field.transient === true || field._meta?.transient === true;
|
|
112
|
-
}
|
|
113
|
-
function isRequiredField(field) {
|
|
114
|
-
if (field.nullable === true || field._meta?.nullable === true) return false;
|
|
115
|
-
return field.required === true || field._meta?.required === true;
|
|
116
|
-
}
|
|
117
|
-
function getCodeDefault(field) {
|
|
118
|
-
if (field.default !== void 0) return { value: field.default };
|
|
119
|
-
if (field._meta?.default !== void 0) return { value: field._meta.default };
|
|
120
|
-
}
|
|
121
|
-
function isUsableRequiredDefault(defaultBox) {
|
|
122
|
-
if (!defaultBox) return false;
|
|
123
|
-
return defaultBox.value !== null && defaultBox.value !== "";
|
|
124
|
-
}
|
|
125
|
-
function buildCodeSeedVisibility(fields) {
|
|
126
|
-
let hasBasicMarkers = false;
|
|
127
|
-
const hints = /* @__PURE__ */ new Map();
|
|
128
|
-
for (const [name, field] of fields) {
|
|
129
|
-
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
130
|
-
hints.set(name, ui);
|
|
131
|
-
if (ui?.basic === true) hasBasicMarkers = true;
|
|
132
|
-
}
|
|
133
|
-
const visibility = /* @__PURE__ */ new Map();
|
|
134
|
-
for (const [name] of fields) {
|
|
135
|
-
const ui = hints.get(name);
|
|
136
|
-
if (ui?.basic === true) visibility.set(name, "basic");
|
|
137
|
-
else if (ui?.basic === false || hasBasicMarkers) visibility.set(name, "advanced");
|
|
138
|
-
else visibility.set(name, "basic");
|
|
139
|
-
}
|
|
140
|
-
return visibility;
|
|
141
|
-
}
|
|
142
|
-
function buildCodeSeedDelta(field, visibility) {
|
|
143
|
-
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
144
|
-
const help = typeof field.description === "string" ? field.description : typeof field._meta?.description === "string" ? field._meta.description : void 0;
|
|
145
|
-
const codeDefault = getCodeDefault(field);
|
|
146
|
-
return {
|
|
147
|
-
...codeDefault ? { default: codeDefault } : {},
|
|
148
|
-
visibility,
|
|
149
|
-
...help !== void 0 ? { help } : {},
|
|
150
|
-
...ui?.order !== void 0 ? { order: ui.order } : {},
|
|
151
|
-
...ui?.locked === true ? { locked: true } : {}
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
function getCodeSeedGroup(field) {
|
|
155
|
-
return sanitizeFieldUIHints(field._meta?.ui)?.group ?? null;
|
|
156
|
-
}
|
|
157
|
-
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
158
|
-
function usesTextIdStorage(field) {
|
|
159
|
-
return field.idType === "text" || field._meta?.idType === "text";
|
|
160
|
-
}
|
|
161
|
-
function assertDefaultValueMatchesFieldType(objectRef, fieldName, field, value) {
|
|
162
|
-
const label = `Field policy default for "${objectRef}.${fieldName}"`;
|
|
163
|
-
if (value === null) {
|
|
164
|
-
if (isRequiredField(field)) throw new Error(`${label} may not be null: the field is required, so a null default is never a usable default`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
switch (field.type) {
|
|
168
|
-
case "text":
|
|
169
|
-
if (typeof value !== "string") throw new Error(`${label} must be a string (field type "text")`);
|
|
170
|
-
return;
|
|
171
|
-
case "integer":
|
|
172
|
-
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer (field type "integer")`);
|
|
173
|
-
return;
|
|
174
|
-
case "decimal":
|
|
175
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number (field type "decimal")`);
|
|
176
|
-
return;
|
|
177
|
-
case "boolean":
|
|
178
|
-
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean (field type "boolean")`);
|
|
179
|
-
return;
|
|
180
|
-
case "datetime":
|
|
181
|
-
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) throw new Error(`${label} must be a date-parseable string (field type "datetime")`);
|
|
182
|
-
return;
|
|
183
|
-
case "json": return;
|
|
184
|
-
case "foreignKey":
|
|
185
|
-
case "crossPackageRef":
|
|
186
|
-
if (typeof value !== "string") throw new Error(`${label} must be a string id (field type "${field.type}")`);
|
|
187
|
-
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)`);
|
|
188
|
-
return;
|
|
189
|
-
default: throw new Error(`${label} is not supported: defaults cannot target "${String(field.type)}" fields`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
//#endregion
|
|
193
|
-
//#region src/models/FieldPolicy.ts
|
|
194
|
-
var __defProp$1 = Object.defineProperty;
|
|
195
|
-
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
196
|
-
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
197
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
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;
|
|
198
31
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
199
|
-
if (kind && result) __defProp$
|
|
32
|
+
if (kind && result) __defProp$5(target, key, result);
|
|
200
33
|
return result;
|
|
201
34
|
};
|
|
202
|
-
var
|
|
35
|
+
var ACTIVE_SUGGESTION_KEY = "active";
|
|
36
|
+
var FieldPolicySuggestion = class extends SmrtObject {
|
|
203
37
|
objectRef = "";
|
|
204
38
|
fieldName = "";
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
locked = null;
|
|
215
|
-
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;
|
|
216
48
|
constructor(options = {}) {
|
|
217
49
|
super(options);
|
|
218
50
|
if (options.objectRef !== void 0) this.objectRef = options.objectRef;
|
|
219
51
|
if (options.fieldName !== void 0) this.fieldName = options.fieldName;
|
|
220
|
-
if (options.scopeType !== void 0) this.scopeType = options.scopeType;
|
|
221
52
|
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
222
|
-
if (options.
|
|
223
|
-
if (options.
|
|
224
|
-
if (options.
|
|
225
|
-
|
|
226
|
-
if (options.
|
|
227
|
-
if (options.
|
|
228
|
-
if (options.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
/** Parse the stored JSON default. `undefined` = no stored default (inherit). */
|
|
234
|
-
getDefaultValue() {
|
|
235
|
-
if (this.defaultValue === null || this.defaultValue === void 0) return;
|
|
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;
|
|
60
|
+
}
|
|
61
|
+
/** Parse the stored evidence object (guarded; junk parses as empty). */
|
|
62
|
+
getEvidence() {
|
|
236
63
|
try {
|
|
237
|
-
|
|
64
|
+
const parsed = JSON.parse(this.evidence);
|
|
65
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
238
66
|
} catch {
|
|
239
|
-
return;
|
|
67
|
+
return {};
|
|
240
68
|
}
|
|
241
69
|
}
|
|
242
|
-
/** Serialize
|
|
243
|
-
|
|
244
|
-
this.
|
|
245
|
-
}
|
|
246
|
-
async save() {
|
|
247
|
-
this.attributeScopeToAmbientContext();
|
|
248
|
-
const previousIdentity = await this.getPersistedIdentity();
|
|
249
|
-
if (previousIdentity) this.assertScopeOwnedByAmbientContext(previousIdentity, "save");
|
|
250
|
-
const ambientContext = getCurrentTenant();
|
|
251
|
-
if (ambientContext) this.updatedBy = ambientContext.userId ?? null;
|
|
252
|
-
this.normalizeDefaultValueForPersistence();
|
|
253
|
-
await this.validateFieldPolicy();
|
|
254
|
-
this.scopeKey = this.computeScopeKey();
|
|
255
|
-
const identityChanged = previousIdentity && (previousIdentity.objectRef !== this.objectRef || previousIdentity.fieldName !== this.fieldName || previousIdentity.scopeType !== this.scopeType || previousIdentity.scopeKey !== this.scopeKey);
|
|
256
|
-
const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange() : await super.save();
|
|
257
|
-
if (identityChanged && previousIdentity) invalidateFieldPolicyCache(previousIdentity.objectRef, this.db);
|
|
258
|
-
invalidateFieldPolicyCache(this.objectRef, this.db);
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
async saveAfterIdentityChange() {
|
|
262
|
-
if (typeof this.db.beginTransaction === "function") return this.saveAfterIdentityChangeInTransaction();
|
|
263
|
-
return this.saveAfterIdentityChangeWithDeferredDelete();
|
|
70
|
+
/** Serialize an evidence object into the stored JSON string. */
|
|
71
|
+
setEvidence(evidence) {
|
|
72
|
+
this.evidence = JSON.stringify(evidence);
|
|
264
73
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const tx = await this.db.beginTransaction?.();
|
|
269
|
-
if (!tx) return this.saveAfterIdentityChangeWithDeferredDelete();
|
|
74
|
+
/** Parse the proposed value; `undefined` when none is stored. */
|
|
75
|
+
getProposedValue() {
|
|
76
|
+
if (this.proposedValue === null || this.proposedValue === void 0) return;
|
|
270
77
|
try {
|
|
271
|
-
this.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const result = await super.save();
|
|
275
|
-
await tx.commit();
|
|
276
|
-
return result;
|
|
277
|
-
} catch (error) {
|
|
278
|
-
try {
|
|
279
|
-
await tx.rollback();
|
|
280
|
-
} catch {}
|
|
281
|
-
throw error;
|
|
282
|
-
} finally {
|
|
283
|
-
this._db = originalDb;
|
|
284
|
-
this.options.db = originalOptionsDb;
|
|
78
|
+
return JSON.parse(this.proposedValue);
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
285
81
|
}
|
|
286
82
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async save() {
|
|
100
|
+
await this.assertRowOwnedByAmbientContext("save");
|
|
101
|
+
await this.validateFieldPolicySuggestion();
|
|
102
|
+
this.applyActiveKey();
|
|
103
|
+
return super.save();
|
|
104
|
+
}
|
|
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;
|
|
304
116
|
}
|
|
117
|
+
if (!this.id) this.id = crypto.randomUUID();
|
|
118
|
+
this.activeKey = String(this.id);
|
|
305
119
|
}
|
|
306
120
|
async delete() {
|
|
307
|
-
|
|
308
|
-
if (persisted) this.assertScopeOwnedByAmbientContext(persisted, "delete");
|
|
309
|
-
const objectRef = persisted?.objectRef ?? this.objectRef;
|
|
121
|
+
await this.assertRowOwnedByAmbientContext("delete");
|
|
310
122
|
await super.delete();
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.
|
|
321
|
-
this.
|
|
123
|
+
}
|
|
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}"`);
|
|
322
137
|
const fieldDef = (await getObjectFieldMap(this.objectRef)).get(this.fieldName);
|
|
323
138
|
if (!fieldDef) throw new Error(`Unknown field "${this.fieldName}" on "${this.objectRef}"`);
|
|
324
|
-
if (fieldDef._meta?.__smrtSystemField === true) throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is
|
|
325
|
-
if (fieldDef
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
331
149
|
assertDefaultValueMatchesFieldType(this.objectRef, this.fieldName, fieldDef, parsed);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
});
|
|
341
176
|
}
|
|
342
|
-
if (needsLockCheck && orgPolicy?.locked) throw new Error(`Field "${this.objectRef}.${this.fieldName}" is locked by org policy; user-scope overrides are not allowed`);
|
|
343
177
|
}
|
|
344
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 {
|
|
345
341
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* precedence implementation — including ancestor-tenant cascades via the
|
|
349
|
-
* default hierarchy loader. The chain tenant is the row's own tenant for
|
|
350
|
-
* tenant-scope rows, the ambient context tenant for user-scope rows, and
|
|
351
|
-
* none for app-scope rows (their only lower layer is the code seed).
|
|
352
|
-
*
|
|
353
|
-
* On updates the resolver sees this row's PERSISTED version (there is no
|
|
354
|
-
* self-exclusion), so a save that removes the only default while demoting
|
|
355
|
-
* can pass here; the resolver-side safety net stays authoritative at read
|
|
356
|
-
* time.
|
|
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.
|
|
357
344
|
*/
|
|
358
|
-
async
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
tenantId
|
|
363
|
-
|
|
364
|
-
}
|
|
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
|
+
};
|
|
365
361
|
}
|
|
366
362
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
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.
|
|
370
381
|
*/
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
};
|
|
375
416
|
}
|
|
376
|
-
if (this.
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
379
431
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
};
|
|
385
441
|
}
|
|
386
442
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* strips it from EVERY generated create/update body, and `FieldPolicy` is
|
|
391
|
-
* deliberately not `@TenantScoped`, so the tenancy interceptor never
|
|
392
|
-
* repopulates it. A `POST {scopeType:'tenant', tenantId}` therefore always
|
|
393
|
-
* reached `validateScopeConsistency` with `tenantId === null`, making the
|
|
394
|
-
* org tier write-dead over every generated surface — the model, as the
|
|
395
|
-
* 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.
|
|
396
446
|
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* 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.
|
|
406
455
|
*/
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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() };
|
|
412
476
|
}
|
|
413
477
|
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
478
|
+
* Write the org-scope policy column an accepted suggestion implies, bound to
|
|
479
|
+
* `db` so it participates in the caller's transaction.
|
|
480
|
+
*
|
|
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:
|
|
417
490
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
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.
|
|
427
501
|
*
|
|
428
|
-
*
|
|
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.
|
|
429
508
|
*/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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;
|
|
435
541
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
542
|
+
}
|
|
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;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
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.
|
|
556
|
+
*/
|
|
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({
|
|
561
|
+
collection: "fields.policy",
|
|
562
|
+
action: "fields.policy.manage".split(".").at(-1) ?? "manage",
|
|
563
|
+
db: this.db,
|
|
439
564
|
tenantId: context.tenantId,
|
|
440
|
-
|
|
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
|
|
441
577
|
});
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
578
|
+
if (suggestion.status !== "pending") throw new FieldPolicySuggestionConflictError(action, id);
|
|
579
|
+
return suggestion;
|
|
580
|
+
}
|
|
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
|
+
}
|
|
445
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;
|
|
446
676
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
677
|
+
/** Parse the stored distinct-user set (guarded; junk parses as empty). */
|
|
678
|
+
getDistinctUserIds() {
|
|
679
|
+
try {
|
|
680
|
+
const parsed = JSON.parse(this.distinctUserIds);
|
|
681
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
|
|
682
|
+
} catch {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
453
685
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
686
|
+
/**
|
|
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;
|
|
459
701
|
}
|
|
460
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Parse the stored histogram (guarded; junk parses as empty).
|
|
704
|
+
*
|
|
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}.
|
|
711
|
+
*/
|
|
712
|
+
getValueHistogram() {
|
|
713
|
+
const histogram = emptyHistogram();
|
|
714
|
+
if (!this.valueHistogram) return histogram;
|
|
461
715
|
try {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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();
|
|
465
722
|
}
|
|
466
723
|
}
|
|
467
724
|
/**
|
|
468
|
-
*
|
|
469
|
-
* key
|
|
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).
|
|
470
728
|
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* lookup always misses, yet the `conflictColumns` upsert still overwrites
|
|
474
|
-
* whatever row already occupies `(objectRef, fieldName, scopeType,
|
|
475
|
-
* scopeKey)`. Authorizing on the primary key alone therefore skipped the
|
|
476
|
-
* persisted-scope guard on exactly the path that can replace an existing
|
|
477
|
-
* row's contents.
|
|
729
|
+
* Bucket presence is an OWN-key test, never a truthiness/`undefined` read,
|
|
730
|
+
* so prototype-shaped ids behave like any other key.
|
|
478
731
|
*/
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return value === void 0 || value === null ? null : String(value);
|
|
491
|
-
};
|
|
492
|
-
return {
|
|
493
|
-
objectRef: read("objectRef", "object_ref", this.objectRef),
|
|
494
|
-
fieldName: read("fieldName", "field_name", this.fieldName),
|
|
495
|
-
scopeType: read("scopeType", "scope_type", this.scopeType),
|
|
496
|
-
scopeKey: read("scopeKey", "scope_key", this.scopeKey),
|
|
497
|
-
tenantId: readNullable("tenantId", "tenant_id"),
|
|
498
|
-
userId: readNullable("userId", "user_id")
|
|
499
|
-
};
|
|
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);
|
|
500
743
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
744
|
+
/**
|
|
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.
|
|
749
|
+
*/
|
|
750
|
+
isLegacyBucket() {
|
|
751
|
+
return this.submissionCount < this.setCount;
|
|
752
|
+
}
|
|
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");
|
|
504
770
|
}
|
|
505
771
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
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).
|
|
509
784
|
*/
|
|
510
|
-
async
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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);
|
|
530
806
|
}
|
|
531
807
|
};
|
|
532
|
-
__decorateClass$
|
|
533
|
-
__decorateClass$
|
|
534
|
-
__decorateClass$
|
|
535
|
-
__decorateClass$
|
|
536
|
-
__decorateClass$
|
|
537
|
-
__decorateClass$
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
})],
|
|
541
|
-
__decorateClass$
|
|
542
|
-
type: "text",
|
|
543
|
-
nullable: true
|
|
544
|
-
})], FieldPolicy.prototype, "defaultValue", 2);
|
|
545
|
-
__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({
|
|
546
818
|
type: "text",
|
|
547
819
|
nullable: true
|
|
548
|
-
})],
|
|
549
|
-
__decorateClass$
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
})], FieldPolicy.prototype, "help", 2);
|
|
553
|
-
__decorateClass$1([field({
|
|
554
|
-
type: "text",
|
|
555
|
-
nullable: true
|
|
556
|
-
})], FieldPolicy.prototype, "label", 2);
|
|
557
|
-
__decorateClass$1([field({
|
|
558
|
-
type: "integer",
|
|
559
|
-
nullable: true
|
|
560
|
-
})], FieldPolicy.prototype, "displayOrder", 2);
|
|
561
|
-
__decorateClass$1([field({
|
|
562
|
-
type: "boolean",
|
|
563
|
-
nullable: true
|
|
564
|
-
})], FieldPolicy.prototype, "locked", 2);
|
|
565
|
-
__decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "updatedBy", 2);
|
|
566
|
-
FieldPolicy = __decorateClass$1([smrt({
|
|
567
|
-
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",
|
|
568
824
|
conflictColumns: [
|
|
569
825
|
"object_ref",
|
|
570
826
|
"field_name",
|
|
571
|
-
"
|
|
572
|
-
"
|
|
827
|
+
"tenant_id",
|
|
828
|
+
"period"
|
|
573
829
|
],
|
|
574
|
-
api: { include: [
|
|
575
|
-
|
|
576
|
-
"update",
|
|
577
|
-
"delete"
|
|
578
|
-
] },
|
|
579
|
-
cli: {
|
|
580
|
-
include: [
|
|
581
|
-
"create",
|
|
582
|
-
"update",
|
|
583
|
-
"delete"
|
|
584
|
-
],
|
|
585
|
-
exclude: ["getDefaultValue", "setDefaultValue"]
|
|
586
|
-
},
|
|
830
|
+
api: { include: [] },
|
|
831
|
+
cli: false,
|
|
587
832
|
mcp: { include: [] }
|
|
588
|
-
})],
|
|
833
|
+
})], FieldUsageCounter);
|
|
589
834
|
//#endregion
|
|
590
|
-
//#region src/
|
|
591
|
-
var __defProp = Object.defineProperty;
|
|
592
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
593
|
-
var
|
|
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,
|
|
875
|
+
mcp: { include: [] }
|
|
876
|
+
})], FieldUsageReportReceipt);
|
|
877
|
+
//#endregion
|
|
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, {
|
|
594
882
|
enumerable: true,
|
|
595
883
|
configurable: true,
|
|
596
884
|
writable: true,
|
|
597
885
|
value
|
|
598
886
|
}) : obj[key] = value;
|
|
599
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
600
|
-
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;
|
|
601
889
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
602
|
-
if (kind && result) __defProp(target, key, result);
|
|
890
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
603
891
|
return result;
|
|
604
892
|
};
|
|
605
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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";
|
|
617
909
|
}
|
|
910
|
+
};
|
|
911
|
+
var FieldUsageCounterCollection = class extends SmrtCollection {
|
|
618
912
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
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.
|
|
621
960
|
*/
|
|
622
|
-
async
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
961
|
+
async reportUsage(options = {}) {
|
|
962
|
+
const entries = validateEntriesInput(options.entries);
|
|
963
|
+
const context = getCurrentTenant();
|
|
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);
|
|
633
973
|
if (!byField) {
|
|
634
974
|
byField = /* @__PURE__ */ new Map();
|
|
635
|
-
|
|
975
|
+
groups.set(entry.objectRef, byField);
|
|
636
976
|
}
|
|
637
|
-
byField.
|
|
977
|
+
const samples = byField.get(entry.fieldName);
|
|
978
|
+
if (samples) samples.push(entry);
|
|
979
|
+
else byField.set(entry.fieldName, [entry]);
|
|
638
980
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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;
|
|
990
|
+
}
|
|
991
|
+
const resolvedFields = await this.resolveDefaultsForCaller(objectRef, tenantId, userId);
|
|
992
|
+
if (!resolvedFields) {
|
|
993
|
+
for (const samples of byField.values()) dropped += samples.length;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
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;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
accepted,
|
|
1038
|
+
dropped
|
|
1039
|
+
};
|
|
651
1040
|
}
|
|
652
1041
|
/**
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
* Caller identity comes exclusively from the ambient tenant context
|
|
657
|
-
* (established by the app's auth hook, e.g. smrt-users' session context) —
|
|
658
|
-
* the request body cannot select another tenant or user (fail closed).
|
|
659
|
-
* Server-side consumers wanting explicit identities call
|
|
660
|
-
* `resolveFieldPolicy()` directly instead.
|
|
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).
|
|
661
1045
|
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
* caller (the generated action route cannot convey per-caller grants to
|
|
666
|
-
* this method, so gated fields fail closed), and transient fields are
|
|
667
|
-
* 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.
|
|
668
1049
|
*/
|
|
669
|
-
async
|
|
670
|
-
const rawRefs = options.objectRefs;
|
|
671
|
-
if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("resolveBatch requires a non-empty \"objectRefs\" string array");
|
|
672
|
-
if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("resolveBatch objectRefs must be non-empty strings");
|
|
673
|
-
const objectRefs = [...new Set(rawRefs)];
|
|
674
|
-
if (objectRefs.length > MAX_BATCH_OBJECT_REFS) throw new Error(`resolveBatch accepts at most ${MAX_BATCH_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
|
|
675
|
-
const context = getCurrentTenant();
|
|
676
|
-
const tenantId = context?.tenantId ?? null;
|
|
677
|
-
const userId = context?.userId ?? null;
|
|
1050
|
+
async resolveDefaultsForCaller(objectRef, tenantId, userId) {
|
|
678
1051
|
const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const resolved = await resolveFieldPolicy(objectRef, {
|
|
1052
|
+
try {
|
|
1053
|
+
return (await resolveFieldPolicy(objectRef, {
|
|
682
1054
|
tenantId,
|
|
683
1055
|
userId,
|
|
684
1056
|
db: this.db
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (!fieldDef) continue;
|
|
696
|
-
if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
|
|
697
|
-
fields[fieldName] = policy;
|
|
1057
|
+
})).fields;
|
|
1058
|
+
} catch {
|
|
1059
|
+
try {
|
|
1060
|
+
return (await resolveFieldPolicy(objectRef, {
|
|
1061
|
+
tenantId,
|
|
1062
|
+
userId
|
|
1063
|
+
})).fields;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
698
1067
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
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.
|
|
1073
|
+
*/
|
|
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"
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
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.
|
|
1089
|
+
*/
|
|
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
|
|
1100
|
+
});
|
|
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();
|
|
703
1107
|
}
|
|
704
1108
|
};
|
|
705
|
-
__publicField(
|
|
706
|
-
|
|
1109
|
+
__publicField$1(FieldUsageCounterCollection, "_itemClass", FieldUsageCounter);
|
|
1110
|
+
FieldUsageCounterCollection = __decorateClass$1([smrt({
|
|
707
1111
|
conflictColumns: [
|
|
708
1112
|
"object_ref",
|
|
709
1113
|
"field_name",
|
|
710
|
-
"
|
|
711
|
-
"
|
|
1114
|
+
"tenant_id",
|
|
1115
|
+
"period"
|
|
712
1116
|
],
|
|
713
1117
|
api: {
|
|
714
|
-
include: [
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
"delete",
|
|
718
|
-
"resolveBatch"
|
|
719
|
-
],
|
|
720
|
-
routes: { resolveBatch: {
|
|
1118
|
+
include: ["reportUsage"],
|
|
1119
|
+
principalContext: true,
|
|
1120
|
+
routes: { reportUsage: {
|
|
721
1121
|
scope: "collection",
|
|
722
1122
|
method: "POST",
|
|
723
|
-
path: "
|
|
1123
|
+
path: "report"
|
|
724
1124
|
} }
|
|
725
1125
|
},
|
|
726
1126
|
cli: false,
|
|
727
1127
|
mcp: false
|
|
728
|
-
})],
|
|
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
|
+
}
|
|
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
|
+
]);
|
|
1177
|
+
}
|
|
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);
|
|
1182
|
+
}
|
|
1183
|
+
function isHistogramEligibleField(field) {
|
|
1184
|
+
return field.type === "boolean" || field.type === "foreignKey" || field.type === "crossPackageRef";
|
|
1185
|
+
}
|
|
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;
|
|
1190
|
+
}
|
|
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;
|
|
1200
|
+
}
|
|
729
1201
|
//#endregion
|
|
730
1202
|
//#region src/field-policy-resolver.ts
|
|
731
1203
|
var field_policy_resolver_exports = /* @__PURE__ */ __exportAll({
|
|
732
1204
|
isMissingUsersDependency: () => isMissingUsersDependency,
|
|
733
1205
|
resolveFieldPolicy: () => resolveFieldPolicy,
|
|
734
|
-
resolveFieldPolicyExplained: () => resolveFieldPolicyExplained
|
|
1206
|
+
resolveFieldPolicyExplained: () => resolveFieldPolicyExplained,
|
|
1207
|
+
resolveSurvivingTenantChainIds: () => resolveSurvivingTenantChainIds
|
|
735
1208
|
});
|
|
736
1209
|
async function resolveFieldPolicy(objectRef, options = {}) {
|
|
737
1210
|
const explained = await resolveFieldPolicyExplained(objectRef, options);
|
|
@@ -740,6 +1213,10 @@ async function resolveFieldPolicy(objectRef, options = {}) {
|
|
|
740
1213
|
fields: explained.fields
|
|
741
1214
|
};
|
|
742
1215
|
}
|
|
1216
|
+
async function resolveSurvivingTenantChainIds(tenantId, options = {}) {
|
|
1217
|
+
assertResolutionAllowedInContext(tenantId, null);
|
|
1218
|
+
return selectSurvivingChainSuffix(await resolveTenantChain(tenantId, options)).map((node) => node.id);
|
|
1219
|
+
}
|
|
743
1220
|
async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
744
1221
|
const tenantId = options.tenantId ?? null;
|
|
745
1222
|
const userId = options.userId ?? null;
|
|
@@ -751,8 +1228,11 @@ async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
|
751
1228
|
cacheDb = collection.db;
|
|
752
1229
|
}
|
|
753
1230
|
const fieldMap = await getObjectFieldMap(objectRef);
|
|
754
|
-
const
|
|
755
|
-
if (
|
|
1231
|
+
const hasExcludedRows = (options.excludePolicyIds?.size ?? 0) > 0;
|
|
1232
|
+
if (!hasExcludedRows) {
|
|
1233
|
+
const cached = getCachedFieldPolicy(objectRef, tenantId, userId, cacheDb, options.tenantHierarchyLoader);
|
|
1234
|
+
if (cached) return cached;
|
|
1235
|
+
}
|
|
756
1236
|
const policyFields = selectPolicyAddressableFields(fieldMap);
|
|
757
1237
|
const codeVisibility = buildCodeSeedVisibility(fieldMap);
|
|
758
1238
|
const appRows = collection ? await collection.getAppRows(objectRef) : /* @__PURE__ */ new Map();
|
|
@@ -774,7 +1254,7 @@ async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
|
774
1254
|
});
|
|
775
1255
|
let state = applyDelta({ visibility: "basic" }, codeDelta);
|
|
776
1256
|
const appRow = appRows.get(fieldName);
|
|
777
|
-
if (appRow) {
|
|
1257
|
+
if (appRow && !options.excludePolicyIds?.has(String(appRow.id))) {
|
|
778
1258
|
const delta = rowToDelta(appRow);
|
|
779
1259
|
contributions.push({
|
|
780
1260
|
layer: "app",
|
|
@@ -784,7 +1264,7 @@ async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
|
784
1264
|
}
|
|
785
1265
|
for (const node of survivingChain) {
|
|
786
1266
|
const row = tenantRows.get(node.id)?.get(fieldName);
|
|
787
|
-
if (row) {
|
|
1267
|
+
if (row && !options.excludePolicyIds?.has(String(row.id))) {
|
|
788
1268
|
const delta = rowToDelta(row);
|
|
789
1269
|
contributions.push({
|
|
790
1270
|
layer: "tenant",
|
|
@@ -796,7 +1276,7 @@ async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
|
796
1276
|
}
|
|
797
1277
|
const orgLocked = state.locked === true;
|
|
798
1278
|
const userRow = userId ? userRows.get(fieldName) : void 0;
|
|
799
|
-
if (userRow && !orgLocked) {
|
|
1279
|
+
if (userRow && !options.excludePolicyIds?.has(String(userRow.id)) && !orgLocked) {
|
|
800
1280
|
const delta = rowToDelta(userRow);
|
|
801
1281
|
contributions.push({
|
|
802
1282
|
layer: "user",
|
|
@@ -834,7 +1314,7 @@ async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
|
834
1314
|
fields,
|
|
835
1315
|
layers
|
|
836
1316
|
};
|
|
837
|
-
setCachedFieldPolicy(objectRef, tenantId, userId, cacheDb, explained, options.tenantHierarchyLoader);
|
|
1317
|
+
if (!hasExcludedRows) setCachedFieldPolicy(objectRef, tenantId, userId, cacheDb, explained, options.tenantHierarchyLoader);
|
|
838
1318
|
return explained;
|
|
839
1319
|
}
|
|
840
1320
|
function assertResolutionAllowedInContext(tenantId, userId) {
|
|
@@ -847,11 +1327,7 @@ function assertResolutionAllowedInContext(tenantId, userId) {
|
|
|
847
1327
|
}
|
|
848
1328
|
function selectPolicyAddressableFields(fieldMap) {
|
|
849
1329
|
const selected = /* @__PURE__ */ new Map();
|
|
850
|
-
for (const [name, field] of fieldMap)
|
|
851
|
-
if (field._meta?.__smrtSystemField === true) continue;
|
|
852
|
-
if (field.type === "oneToMany" || field.type === "manyToMany" || field.type === "meta") continue;
|
|
853
|
-
selected.set(name, field);
|
|
854
|
-
}
|
|
1330
|
+
for (const [name, field] of fieldMap) if (isPolicyAddressableField(field)) selected.set(name, field);
|
|
855
1331
|
return selected;
|
|
856
1332
|
}
|
|
857
1333
|
function rowToDelta(row) {
|
|
@@ -914,7 +1390,7 @@ async function defaultTenantHierarchyLoader(options) {
|
|
|
914
1390
|
throw error;
|
|
915
1391
|
}
|
|
916
1392
|
}
|
|
917
|
-
var MISSING_MODULE_TARGET_PATTERN = /Cannot find (?:package|module) '([^']+)'/;
|
|
1393
|
+
var MISSING_MODULE_TARGET_PATTERN$1 = /Cannot find (?:package|module) '([^']+)'/;
|
|
918
1394
|
function isUsersSpecifier(target) {
|
|
919
1395
|
return target === "@happyvertical/smrt-users" || target.startsWith("@happyvertical/smrt-users/");
|
|
920
1396
|
}
|
|
@@ -923,7 +1399,7 @@ function isMissingUsersDependency(error) {
|
|
|
923
1399
|
const seen = /* @__PURE__ */ new Set();
|
|
924
1400
|
while (current instanceof Error && !seen.has(current)) {
|
|
925
1401
|
seen.add(current);
|
|
926
|
-
const match = current.message.match(MISSING_MODULE_TARGET_PATTERN);
|
|
1402
|
+
const match = current.message.match(MISSING_MODULE_TARGET_PATTERN$1);
|
|
927
1403
|
if (match && isUsersSpecifier(match[1])) return true;
|
|
928
1404
|
if (current.message.includes("Failed to load @happyvertical/smrt-users for")) return true;
|
|
929
1405
|
current = current.cause;
|
|
@@ -931,6 +1407,715 @@ function isMissingUsersDependency(error) {
|
|
|
931
1407
|
return false;
|
|
932
1408
|
}
|
|
933
1409
|
//#endregion
|
|
934
|
-
|
|
1410
|
+
//#region src/settings-catalog.ts
|
|
1411
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
1412
|
+
var MAX_PAGE_SIZE = 100;
|
|
1413
|
+
var FORM_TYPES = /* @__PURE__ */ new Set([
|
|
1414
|
+
"text",
|
|
1415
|
+
"integer",
|
|
1416
|
+
"decimal",
|
|
1417
|
+
"boolean",
|
|
1418
|
+
"datetime",
|
|
1419
|
+
"json",
|
|
1420
|
+
"foreignKey",
|
|
1421
|
+
"crossPackageRef"
|
|
1422
|
+
]);
|
|
1423
|
+
var SYSTEM_NAMES = /* @__PURE__ */ new Set([
|
|
1424
|
+
"id",
|
|
1425
|
+
"slug",
|
|
1426
|
+
"context",
|
|
1427
|
+
"createdAt",
|
|
1428
|
+
"created_at",
|
|
1429
|
+
"updatedAt",
|
|
1430
|
+
"updated_at",
|
|
1431
|
+
"deletedAt",
|
|
1432
|
+
"deleted_at",
|
|
1433
|
+
"tenantId",
|
|
1434
|
+
"tenant_id"
|
|
1435
|
+
]);
|
|
1436
|
+
function fieldPolicyCatalogItemId(objectRef, fieldName) {
|
|
1437
|
+
return `${objectRef}::${fieldName}`;
|
|
1438
|
+
}
|
|
1439
|
+
function parseFieldPolicyCatalogQuery(params) {
|
|
1440
|
+
return {
|
|
1441
|
+
query: params.get("q"),
|
|
1442
|
+
page: integerParam(params.get("page")),
|
|
1443
|
+
pageSize: integerParam(params.get("pageSize")),
|
|
1444
|
+
selectedId: params.get("selected"),
|
|
1445
|
+
packageFilter: params.get("package"),
|
|
1446
|
+
objectFilter: params.get("object"),
|
|
1447
|
+
customizedOnly: params.get("customized") === "1"
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
async function buildFieldPolicySettingsCatalog(options) {
|
|
1451
|
+
const collection = options.collection ?? (options.db ? await FieldPolicyCollection.create({ db: options.db }) : null);
|
|
1452
|
+
if (!collection) throw new Error("A db or FieldPolicyCollection is required");
|
|
1453
|
+
const filters = {
|
|
1454
|
+
packageFilter: stringFilter(options.packageFilter),
|
|
1455
|
+
objectFilter: stringFilter(options.objectFilter),
|
|
1456
|
+
customizedOnly: options.customizedOnly === true
|
|
1457
|
+
};
|
|
1458
|
+
const query = options.query?.trim() ?? "";
|
|
1459
|
+
const pageSize = clamp(options.pageSize, 1, MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE);
|
|
1460
|
+
const baseAudit = await collection.policyAudit({ summaryOnly: true });
|
|
1461
|
+
if (!baseAudit.caller.canManageOrg) return {
|
|
1462
|
+
page: emptyPage(query, pageSize),
|
|
1463
|
+
audit: baseAudit,
|
|
1464
|
+
objects: [],
|
|
1465
|
+
packages: [],
|
|
1466
|
+
filters
|
|
1467
|
+
};
|
|
1468
|
+
const objects = await listCatalogObjects(options.objectRefs);
|
|
1469
|
+
const candidates = objects.filter((object) => (!filters.packageFilter || object.packageName === filters.packageFilter) && (!filters.objectFilter || object.objectRef === filters.objectFilter));
|
|
1470
|
+
const allCandidateRefs = candidates.map((object) => object.objectRef);
|
|
1471
|
+
const customized = customizedKeys(filters.customizedOnly && allCandidateRefs.length ? await loadAuditCounts(collection, allCandidateRefs, baseAudit) : baseAudit);
|
|
1472
|
+
const entries = candidates.flatMap((object) => Object.entries(object.fields).filter(([fieldName]) => !filters.customizedOnly || customized.has(fieldPolicyCatalogItemId(object.objectRef, fieldName))).map(([fieldName, field]) => ({
|
|
1473
|
+
item: {
|
|
1474
|
+
id: fieldPolicyCatalogItemId(object.objectRef, fieldName),
|
|
1475
|
+
label: fieldName,
|
|
1476
|
+
...field.description ? { description: field.description } : {},
|
|
1477
|
+
eyebrow: `${object.className} \xB7 ${object.packageName}`,
|
|
1478
|
+
objectRef: object.objectRef,
|
|
1479
|
+
fieldName,
|
|
1480
|
+
className: object.className,
|
|
1481
|
+
packageName: object.packageName
|
|
1482
|
+
},
|
|
1483
|
+
search: `${fieldName} ${spaced(fieldName)} ${object.className} ${object.packageName} ${object.objectRef} ${field.description ?? ""}`.toLowerCase()
|
|
1484
|
+
})));
|
|
1485
|
+
const filtered = query ? entries.filter((entry) => entry.search.includes(query.toLowerCase())) : entries;
|
|
1486
|
+
const total = filtered.length;
|
|
1487
|
+
const page = clamp(options.page, 1, Math.max(1, Math.ceil(total / pageSize)), 1);
|
|
1488
|
+
const items = filtered.slice((page - 1) * pageSize, page * pageSize).map((entry) => entry.item);
|
|
1489
|
+
const selectedSummary = (options.selectedId ? filtered.find((entry) => entry.item.id === options.selectedId)?.item : void 0) ?? items[0];
|
|
1490
|
+
const selected = selectedSummary ? {
|
|
1491
|
+
...selectedSummary,
|
|
1492
|
+
fields: objects.find((object) => object.objectRef === selectedSummary.objectRef)?.fields ?? {}
|
|
1493
|
+
} : null;
|
|
1494
|
+
const auditRefs = selected ? uniqueRefs([selected.objectRef, ...items.map((item) => item.objectRef)]) : [];
|
|
1495
|
+
const audit = auditRefs.length ? await collection.policyAudit({
|
|
1496
|
+
objectRefs: auditRefs.slice(0, 100),
|
|
1497
|
+
countObjectRefs: auditRefs,
|
|
1498
|
+
includeDrift: true
|
|
1499
|
+
}) : await collection.policyAudit({ includeDrift: true });
|
|
1500
|
+
return {
|
|
1501
|
+
page: {
|
|
1502
|
+
items,
|
|
1503
|
+
selected,
|
|
1504
|
+
query,
|
|
1505
|
+
page,
|
|
1506
|
+
pageSize,
|
|
1507
|
+
total
|
|
1508
|
+
},
|
|
1509
|
+
audit,
|
|
1510
|
+
objects: objects.map(({ objectRef, className, packageName, fields }) => ({
|
|
1511
|
+
objectRef,
|
|
1512
|
+
className,
|
|
1513
|
+
packageName,
|
|
1514
|
+
fieldCount: Object.keys(fields).length
|
|
1515
|
+
})),
|
|
1516
|
+
packages: [...new Set(objects.map((object) => object.packageName))].sort(),
|
|
1517
|
+
filters
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
async function listCatalogObjects(objectRefs) {
|
|
1521
|
+
const refs = objectRefs ? [...objectRefs] : Array.from(ObjectRegistry.getPublicClasses().values()).reduce((result, registered) => {
|
|
1522
|
+
if (registered.qualifiedName) result.push(registered.qualifiedName);
|
|
1523
|
+
return result;
|
|
1524
|
+
}, []);
|
|
1525
|
+
const unique = [...new Set(refs)].sort();
|
|
1526
|
+
const objects = [];
|
|
1527
|
+
for (const objectRef of unique) {
|
|
1528
|
+
if (!ObjectRegistry.getClassByQualifiedName(objectRef) || ObjectRegistry.getTableName(objectRef)?.startsWith("_smrt_")) continue;
|
|
1529
|
+
const fields = await getObjectFieldMap(objectRef);
|
|
1530
|
+
const selected = {};
|
|
1531
|
+
for (const [name, definition] of fields) {
|
|
1532
|
+
if (SYSTEM_NAMES.has(name) || !isPolicyAddressableField(definition) || isSensitiveField(definition) || isTransientField(definition) || getFieldReadPermission(definition) !== void 0 || !FORM_TYPES.has(String(definition.type))) continue;
|
|
1533
|
+
selected[name] = {
|
|
1534
|
+
type: definition.type,
|
|
1535
|
+
...definition.required === true ? { required: true } : {},
|
|
1536
|
+
...definition.default !== void 0 ? { default: definition.default } : {},
|
|
1537
|
+
...typeof definition.description === "string" ? { description: definition.description } : {}
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
if (!Object.keys(selected).length) continue;
|
|
1541
|
+
const colon = objectRef.lastIndexOf(":");
|
|
1542
|
+
const packageName = colon === -1 ? "" : objectRef.slice(0, colon);
|
|
1543
|
+
const className = colon === -1 ? objectRef : objectRef.slice(colon + 1);
|
|
1544
|
+
objects.push({
|
|
1545
|
+
objectRef,
|
|
1546
|
+
className,
|
|
1547
|
+
packageName,
|
|
1548
|
+
fields: selected
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
return objects;
|
|
1552
|
+
}
|
|
1553
|
+
async function loadAuditCounts(collection, refs, baseline) {
|
|
1554
|
+
const userOverrideCounts = {};
|
|
1555
|
+
const orgRows = [];
|
|
1556
|
+
const appRows = [];
|
|
1557
|
+
const inheritedOrgKeys = {};
|
|
1558
|
+
const chunkSize = 100;
|
|
1559
|
+
for (let offset = 0; offset < refs.length; offset += chunkSize) {
|
|
1560
|
+
const audit = await collection.policyAudit({
|
|
1561
|
+
objectRefs: refs.slice(offset, offset + chunkSize),
|
|
1562
|
+
countObjectRefs: refs.slice(offset, offset + chunkSize),
|
|
1563
|
+
countsOnly: true
|
|
1564
|
+
});
|
|
1565
|
+
for (const [objectRef, byField] of Object.entries(audit.userOverrideCounts)) userOverrideCounts[objectRef] = {
|
|
1566
|
+
...userOverrideCounts[objectRef] ?? {},
|
|
1567
|
+
...byField
|
|
1568
|
+
};
|
|
1569
|
+
orgRows.push(...audit.orgRows);
|
|
1570
|
+
appRows.push(...audit.appRows);
|
|
1571
|
+
for (const [objectRef, fieldNames] of Object.entries(audit.inheritedOrgKeys)) {
|
|
1572
|
+
const names = inheritedOrgKeys[objectRef] ?? [];
|
|
1573
|
+
inheritedOrgKeys[objectRef] = names;
|
|
1574
|
+
for (const fieldName of fieldNames) if (!names.includes(fieldName)) names.push(fieldName);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return {
|
|
1578
|
+
...baseline,
|
|
1579
|
+
orgRows,
|
|
1580
|
+
appRows,
|
|
1581
|
+
inheritedOrgKeys,
|
|
1582
|
+
userOverrideCounts
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
function customizedKeys(audit) {
|
|
1586
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1587
|
+
for (const row of [...audit.orgRows, ...audit.appRows]) keys.add(fieldPolicyCatalogItemId(row.objectRef, row.fieldName));
|
|
1588
|
+
for (const [objectRef, names] of Object.entries(audit.inheritedOrgKeys)) for (const name of names) keys.add(fieldPolicyCatalogItemId(objectRef, name));
|
|
1589
|
+
for (const [objectRef, fields] of Object.entries(audit.userOverrideCounts)) for (const [name, count] of Object.entries(fields)) if (count > 0) keys.add(fieldPolicyCatalogItemId(objectRef, name));
|
|
1590
|
+
return keys;
|
|
1591
|
+
}
|
|
1592
|
+
function emptyPage(query, pageSize) {
|
|
1593
|
+
return {
|
|
1594
|
+
items: [],
|
|
1595
|
+
selected: null,
|
|
1596
|
+
query,
|
|
1597
|
+
page: 1,
|
|
1598
|
+
pageSize,
|
|
1599
|
+
total: 0
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function uniqueRefs(refs) {
|
|
1603
|
+
return [...new Set(refs)];
|
|
1604
|
+
}
|
|
1605
|
+
function stringFilter(value) {
|
|
1606
|
+
const trimmed = value?.trim();
|
|
1607
|
+
return trimmed ? trimmed : null;
|
|
1608
|
+
}
|
|
1609
|
+
function integerParam(value) {
|
|
1610
|
+
return value && /^\d+$/.test(value) ? Number(value) : null;
|
|
1611
|
+
}
|
|
1612
|
+
function clamp(value, min, max, fallback) {
|
|
1613
|
+
return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : fallback;
|
|
1614
|
+
}
|
|
1615
|
+
function spaced(value) {
|
|
1616
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ");
|
|
1617
|
+
}
|
|
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
|
|
2116
|
+
//#region src/index.ts
|
|
2117
|
+
ensureFieldPolicyPermissionsRegistered();
|
|
2118
|
+
//#endregion
|
|
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 };
|
|
935
2120
|
|
|
936
2121
|
//# sourceMappingURL=index.js.map
|