Skip to content

feat: add a first-class user field type (person picker)#2351

Merged
os-zhuang merged 3 commits into
mainfrom
feat/user-field-type
Jun 26, 2026
Merged

feat: add a first-class user field type (person picker)#2351
os-zhuang merged 3 commits into
mainfrom
feat/user-field-type

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Adds a user field type — the equivalent of Airtable's Collaborator / Notion's Person / Salesforce's Lookup(User). Authored as Field.user({ ... }); { multiple: true } for collaborators/watchers and { defaultValue: 'current_user' } to auto-fill the acting user on create.

Why a distinct type (not just Field.lookup('sys_user'))

Selecting a person is table-stakes, but you can already store one via a lookup — owner_id/created_by already do. The value is modelling discoverability: a "User" entry in the Studio/AI field palette instead of requiring authors (and the AI authoring path) to know to reference the internal sys_user system object — plus current_user defaults and a user-search picker.

Design: a semantic specialization of lookup, not a new storage primitive

user shares the exact lookup code path — same FK string column (multiple ⇒ JSON), same $expand resolution, same indexing. Consequences:

  • Referential integrity + always-fresh display names come for free (vs. an inline collaborator blob that goes stale).
  • An existing Field.lookup('sys_user') is equivalent at the storage layer → zero data migration to adopt Field.user.
  • Nothing in the lookup machinery is re-implemented.

This mirrors how relational platforms (Salesforce, ServiceNow, Dataverse) model users as a reference to the user object, while presenting a first-class type at the authoring/UX layer like Airtable/Notion.

Ownership left unchanged

A declarative owner field type was intentionally not added. Ownership is a system role (one per object, auto-stamped) already handled by the owner_id convention + plugin-security auto-stamp/RLS; a second type would only grow the platform-wide FieldType surface for marginal benefit.

Changes

  • spec: FieldType gains 'user' + Field.user() builder; seed-loader / GraphQL / SQL type-compat reference maps accept user.
  • drivers (sql, mongodb): treat user exactly like lookup.
  • engine: resolve $expand for user fields; honour a new defaultValue: 'current_user' token (resolved app-side from the execution context, mirroring the NOW() convention).
  • misc: kanban group-by, symbolic seed references, and approvals enrichment accept user.

Tests

  • engine: $expand-for-user; current_user stamp (present / absent actor).
  • driver-sql: string-id + multiple ⇒ JSON round-trip.
  • 713 objectql + 234 driver-sql pass; api-surface gate green (additive enum member — public surface unchanged).

Paired UI

objectui feat/user-field-picker wires the UserField picker to sys_user.

🤖 Generated with Claude Code

A `user` field type — the equivalent of Airtable's Collaborator / Notion's
Person / Salesforce's `Lookup(User)`. Authored as `Field.user({ ... })`;
`{ multiple: true }` for collaborators/watchers and
`{ defaultValue: 'current_user' }` to auto-fill the acting user on create.

Designed as a SEMANTIC SPECIALIZATION OF `lookup` with the target fixed to the
`sys_user` system object — NOT a new storage primitive. It shares the exact
lookup code path: same FK string column (multiple ⇒ JSON), same `$expand`
resolution, same indexing. So referential integrity and fresh display names
come for free, an existing `Field.lookup('sys_user')` is equivalent at the
storage layer (zero data migration), and nothing is re-implemented.

The distinct type exists for modelling discoverability — a "User" entry in the
Studio/AI field palette instead of requiring authors (and AI) to know to
reference the internal `sys_user` object — plus a user-search picker and
`current_user` defaults.

Ownership semantics are unchanged: the existing `owner_id` convention +
plugin-security auto-stamp/RLS still apply. A declarative `owner` field type
was intentionally NOT added — ownership is a system role (one per object,
auto-stamped), already handled, and a second type would only grow the
platform-wide `FieldType` surface for marginal benefit.

Changes:
- spec: `FieldType` gains `'user'` + `Field.user()` builder; seed-loader /
  GraphQL / SQL type-compat reference maps accept `user`.
- drivers (sql, mongodb): treat `user` exactly like `lookup`.
- engine: resolve `$expand` for `user` fields; honour a new
  `defaultValue: 'current_user'` token (resolved app-side from the execution
  context, mirroring the `NOW()` convention).
- kanban group-by + symbolic seed references + approvals enrichment accept `user`.

Tests: engine $expand-for-user + current_user stamp (present / absent actor);
driver-sql string-id + multiple⇒JSON round-trip. Public API surface unchanged
(additive enum member).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 26, 2026 4:45pm

Request Review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/cli, @objectstack/objectql, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec.

100 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/cloud-artifact-api.mdx (via packages/cli, packages/spec)
  • content/docs/concepts/cluster-semantics.mdx (via @objectstack/spec)
  • content/docs/concepts/core/plugins.mdx (via @objectstack/driver-sql)
  • content/docs/concepts/core/services.mdx (via @objectstack/objectql)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/cli, @objectstack/objectql, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/concepts/packages.mdx (via @objectstack/cli, @objectstack/objectql, @objectstack/driver-mongodb, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec)
  • content/docs/concepts/setup-app.mdx (via @objectstack/spec)
  • content/docs/concepts/skills.mdx (via @objectstack/spec)
  • content/docs/concepts/terminology.mdx (via @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/concepts/webhook-delivery.mdx (via @objectstack/spec)
  • content/docs/getting-started/architecture.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/getting-started/core-concepts.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/guides/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/guides/ai-capabilities.mdx (via @objectstack/spec)
  • content/docs/guides/airtable-dashboard-analysis.mdx (via @objectstack/spec)
  • content/docs/guides/analytics-datasets.mdx (via @objectstack/spec)
  • content/docs/guides/api-reference.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/guides/authentication.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/guides/business-logic.mdx (via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/guides/cheatsheets/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/error-catalog.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-type-gallery.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-validation-rules.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/protocol-diagram.mdx (via packages/spec)
  • content/docs/guides/cheatsheets/query-cheat-sheet.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/quick-reference.mdx (via @objectstack/spec)
  • content/docs/guides/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/guides/common-patterns.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/auth-service.mdx (via packages/spec)
  • content/docs/guides/contracts/cache-service.mdx (via packages/spec)
  • content/docs/guides/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/index.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/guides/contracts/storage-service.mdx (via packages/spec)
  • content/docs/guides/data-modeling.mdx (via @objectstack/spec)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/guides/driver-configuration.mdx (via @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/guides/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/guides/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/guides/external-datasources.mdx (via @objectstack/spec)
  • content/docs/guides/formula.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/guides/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/guides/kernel-services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/guides/metadata/dashboard.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/field.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/flow.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/index.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/object.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/validation.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/workflow.mdx (via @objectstack/spec)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/objectql)
  • content/docs/guides/packages.mdx (via @objectstack/cli, @objectstack/objectql, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec)
  • content/docs/guides/plugin-development.mdx (via @objectstack/spec)
  • content/docs/guides/plugins.mdx (via @objectstack/objectql, @objectstack/rest, @objectstack/spec)
  • content/docs/guides/project-scoping.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/guides/public-forms.mdx (via @objectstack/spec)
  • content/docs/guides/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/guides/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/guides/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/guides/security.mdx (via @objectstack/spec)
  • content/docs/guides/seed-data.mdx (via @objectstack/spec)
  • content/docs/guides/skills.mdx (via packages/cli, @objectstack/spec)
  • content/docs/guides/standards.mdx (via @objectstack/spec)
  • content/docs/guides/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/guides/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/objectql, @objectstack/driver-sql)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/objectos/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`field-zoo` is the exhaustive FieldType coverage object, and
`coverage.test.ts` asserts every member of `FieldType` appears across the
showcase objects. Adding `'user'` to the enum without a representative field
made "covers every FieldType" fail (uncovered → user). Add single, multiple,
and `current_user`-default `user` fields so the guard is satisfied and the new
type gets real data-layer coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public-form schema endpoint strips `lookup`/`master_detail` fields from
anonymous forms unless the designer opts in via `publicPicker` — so a stray
spec mistake can't expose unrestricted record search to the internet. A `user`
field is a lookup specialized to `sys_user`, so the same risk applies: it would
surface a user search/list to unauthenticated visitors. Gate it behind the same
`publicPicker` opt-in. (The companion `/forms/:slug/lookup/:field` endpoint
already 403s without `publicPicker`, independent of type — this closes the
form-schema side.)

Found while dogfooding the new `user` field type end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@os-zhuang os-zhuang merged commit d980f0d into main Jun 26, 2026
17 checks passed
@os-zhuang os-zhuang deleted the feat/user-field-type branch June 26, 2026 16:53
os-zhuang added a commit that referenced this pull request Jun 26, 2026
Follow-up to #2351 (the `user` field type). Brings the docs in line with the
shipped feature:

- references/data/field.mdx + seed-loader.mdx — regenerated (`gen:docs`) so the
  FieldType / reference enums list `user` (the merge added the type but didn't
  regenerate these committed reference pages).
- guides/data-modeling.mdx — new "User Fields" section: `Field.user()`,
  `multiple`, `defaultValue: 'current_user'`, and that it's a lookup specialized
  to sys_user (stored as an FK, zero-migration from `Field.lookup('sys_user')`).
- guides/standards.mdx — adds a User/Person row to the field-type selection table.

Only the user-related regenerated lines are included; unrelated generated-doc
drift was intentionally left out.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant