s-m-r-t
Open the s-m-r-t source on GitHub Switch to dark color scheme
← Reference

Reference

Field policy exports and semantics

This page explains the @happyvertical/smrt-fields exports, object names, wire encoding, write validation, and result caches.

Entry points

The package root holds the model, the collection, the resolver, and the helpers. The /svelte subpath holds the eleven components and the browser-side helpers. /manifest exposes the generated manifest and /playground the shared playground module.

  • Models and collection: FieldPolicy, FieldPolicyCollection.
  • Resolution: resolveFieldPolicy, resolveFieldPolicyExplained, resolveSurvivingTenantChainIds.
  • Cache control: clearFieldPolicyCache, invalidateFieldPolicyCache, getFieldPolicyCacheTtlMs.
  • Permissions: MANAGE_FIELD_POLICY_PERMISSION, PERSONALIZE_FIELD_POLICY_PERMISSION, FIELD_POLICY_PERMISSION_DEFINITIONS, ensureFieldPolicyPermissionsRegistered.
  • Field helpers: isPolicyAddressableField, isRequiredField, isSensitiveField, isTransientField, getCodeDefault, buildCodeSeedDelta, buildCodeSeedVisibility, sanitizeFieldUIHints, assertDefaultValueMatchesFieldType.
  • Control panel: buildFieldPolicySettingsCatalog, parseFieldPolicyCatalogQuery, fieldPolicyCatalogItemId.
src/lib/field-policy.ts
typescript
import {
  FieldPolicy,
  FieldPolicyCollection,
  MANAGE_FIELD_POLICY_PERMISSION,
  resolveFieldPolicy,
  resolveFieldPolicyExplained
} from '@happyvertical/smrt-fields';

import {
  createFieldInputRegistry,
  FieldPolicyControlPanel,
  FieldPolicyGearProvider,
  ObjectForm,
  policyToVisibleColumnIds
} from '@happyvertical/smrt-fields/svelte';

Components in the /svelte subpath

Eleven components ship in the /svelte subpath, each with a matching Props type. They are layout-neutral: they contribute behavior and leave markup decisions to the application.

  • Form primitives: FieldPolicyProvider, PolicyField, ModeSwitch, AdvancedFields, FormHelp.
  • Generated forms: ObjectForm, ObjectFormSourceProvider.
  • Administration: FieldPolicyGearProvider, FieldPolicyGearButton, FieldPolicyEditor, FieldPolicyControlPanel.

Naming an object

Every policy row and every resolve call names its object with the canonical qualified class name: the package name, a colon, then the class name. It is the same identifier the manifest uses, so it survives renames of tables and routes.

object-refs.ts
typescript
'@happyvertical/smrt-content:Article'
'@happyvertical/smrt-commerce:Invoice'
'@acme/app-objects:StarterAppSetting'

Two channels for a default value

defaultValue is the encoded channel: already JSON, exactly as the column stores it. defaultValueRaw is the plain channel: any value, always serialized. They are mutually exclusive and passing both throws, because one option cannot distinguish the string TBD from the encoded JSON string that contains it.

  • Generated write routes hand the request body straight to the constructor, so the wire contract has to stay encoded.
  • setDefaultValue is the method-level plain channel.
  • The sort column is displayOrder; the resolved output exposes it as order.
default-values.ts
typescript
// Encoded channel — what a generated route or the gear posts
new FieldPolicy({ defaultValue: JSON.stringify('Net 30') });

// Plain channel — what application code usually wants
new FieldPolicy({ defaultValueRaw: 'Net 30' });

// Passing both throws; { defaultValue: 'Net 30' } is a parse error
// whose message names defaultValueRaw.

What a write is validated against

Writes validate against the live object registry rather than a checked-in manifest file. An unknown object or field is rejected, a default is type-checked against the field type, and defaults are refused on transient, sensitive, and read-permission-gated fields.

  • System fields, relationship pseudo-fields, and single-table-inheritance meta storage fields are not policy-addressable.
  • Required fields may only leave the basic tier when a usable default resolves, checked at write time and re-checked at resolution.
  • locked may be written on app and tenant rows only.

Generated surface

FieldPolicy generates create, update, and delete routes. Generated list and get are deliberately closed: the model is not tenant-scoped, so a browsable read would enumerate every tenant and user row. Reads happen through three collection actions or through the server-side resolver.

  • POST <collection>/resolve — resolveBatch, the context-scoped policy read used to bootstrap forms.
  • POST <collection>/editor-state — getEditorState, the gear bootstrap.
  • POST <collection>/policy-audit — policyAudit, the manage-gated organization roll-up.
  • MCP is closed entirely. The generated CLI supports only create, update, and delete. A CLI command needs an API route, and the read routes do not exist. The collection configuration also closes the runtime CLI surface.

Caching and invalidation

Resolved results are cached for a short TTL per database namespace, object reference, tenant, user, and hierarchy loader. The loader is part of the key because an injected loader yields a different ancestor chain and therefore different defaults and locks.

  • Saving or deleting a row invalidates every entry for that object, because a parent-tenant row affects each descendant.
  • Policy rows do not ride the client change feed, so a browser learns about a change on its next resolve.
  • Write-time validation resolves projected state outside the shared cache so a hypothetical result can never poison a real read.

Version prerequisite

@happyvertical/smrt-fields first appeared in the 0.40.5x line and is not part of 0.39.x. The package pins smrt-core, smrt-tenancy, smrt-ui, and smrt-users to its exact version. Install it at the same version as the other s-m-r-t packages. A mismatch installs a second object registry, and policy resolution stops recognizing your objects.

  • smrt-users is a required runtime dependency, not an optional one: the permission catalog and operation guard back every write and gear action.
  • The usage-learning loop is not in any published release yet.