Skip to content

feat(database): support Created By and Last Edited By fields - #480

Merged
appflowy merged 3 commits into
mainfrom
codex/database-created-edited-by-web
Aug 18, 2026
Merged

feat(database): support Created By and Last Edited By fields#480
appflowy merged 3 commits into
mainfrom
codex/database-created-edited-by-web

Conversation

@appflowy

@appflowy appflowy commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Adds first-class, read-only Created By and Last Edited By database properties with web/server-compatible row attribution semantics.

  • Stores exact signed-64-bit user IDs as canonical decimal strings in primitive Yjs row fields, avoiding JavaScript precision loss while allowing Yjs garbage collection.
  • Initializes both values for new rows and row templates, preserves the creator, and updates only the last editor for cell, relation, row metadata, and row-document edits.
  • Avoids falsely attributing remote row-document updates to the local user.
  • Renders attribution users across grid cells and cards, and supports the fields in filters, groups, and sorts.
  • Refreshes the mentionable-user cache once when an attributed user is missing so cells resolve the current username rather than a stale fallback.
  • Deduplicates concurrent mentionable-user disk/API/persistence work and indexes the shared member snapshot by exact UID for constant-time cell resolution.
  • Adds multi-user Playwright BDD coverage: Nathan creates and edits a row, Eva edits it later, and both sessions verify Created By = Nathan and Last Edited By = Eva.

The corresponding server/client-api change supplies these fields for imports, default database templates, and server-created rows.

Validation

  • pnpm type-check
  • Targeted typed ESLint across the changed application TypeScript/TSX files and BDD step
  • 9 focused Jest suites: 356 tests passed
  • Playwright BDD row-attribution scenario: 1 passed
  • git diff --check

Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Introduce lossless, read-only row attribution fields that identify who created and most recently edited each database row.

New Features:

  • Add read-only Created by and Last edited by database fields with attribution displayed in cells and cards.
  • Support attribution fields in database filters, grouping, sorting, templates, and row creation workflows.

Bug Fixes:

  • Prevent remote row-document changes from being attributed to the local user.
  • Preserve exact large user IDs without JavaScript precision loss.

Enhancements:

  • Track row creators and last editors across cell, relation, metadata, duplication, and document edits while retaining creator identity.
  • Improve mentionable-user caching, UID indexing, and refresh behavior for resolving attributed users.

Tests:

  • Add unit and integration coverage for attribution storage, filtering, grouping, sorting, user resolution, caching, and local-versus-remote edits.
  • Add a multi-user Playwright scenario verifying creator and last-editor behavior across sessions.

@sourcery-ai

sourcery-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds Created By and Last Edited By attribution fields as first-class, read-only database properties, wiring them through row initialization, cell/row/relation updates, grouping/filtering/sorting, user cache handling, UI rendering, and E2E tests while preserving correct web/server attribution semantics.

Sequence diagram for database row attribution lifecycle

sequenceDiagram
  actor Nathan
  actor Eva
  participant WebApp
  participant YDatabaseRow as YDatabaseRow
  participant Attribution as attribution_ts
  participant MentionCache as useMentionableUsersWithAutoFetch

  Nathan->>WebApp: useNewRowDispatch / createRow
  WebApp->>YDatabaseRow: initialDatabaseRow(rowId, dbId, rowDoc, uid)
  WebApp->>Attribution: initializeRowAttribution(row, uid)
  Attribution->>YDatabaseRow: set created_by
  Attribution->>YDatabaseRow: set last_edited_by

  Eva->>WebApp: useUpdateCellDispatch(rowId, fieldId)
  WebApp->>YDatabaseRow: writeCellToRow(..., actorUid)
  WebApp->>Attribution: touchRowAttribution(row, uid)
  Attribution->>YDatabaseRow: set last_modified
  Attribution->>YDatabaseRow: set last_edited_by

  WebApp->>AttributionCell: render CreatedBy/LastEditedBy
  AttributionCell->>YDatabaseRow: get created_by / last_edited_by
  AttributionCell->>Attribution: normalizeAttributionUid(uid)
  AttributionCell->>MentionCache: useMentionableUsersWithAutoFetch(shouldFetch, requiredUid)
  MentionCache->>WorkspaceService: getMentionableUsers(workspaceId)
  MentionCache-->>AttributionCell: mentionableUsers (resolved names)
  AttributionCell-->>WebApp: display creator/editor names
Loading

File-Level Changes

Change Details Files
Introduce canonical user-id handling and row attribution helpers for Created By / Last Edited By metadata.
  • Add canonicalizeUserUid utility to normalize large numeric user IDs to safe canonical strings and reuse it across user, workspace, and mention APIs.
  • Define AttributionUid utilities (normalizeAttributionUid, initializeRowAttribution, touchRowAttribution) to initialize creator/editor metadata and update last_edited_by + last_modified consistently.
  • Extend Yjs database row types and keys to store created_by/last_edited_by as primitive values and update initialDatabaseRow to set attribution on creation when an actor is available.
src/application/user-uid.ts
src/application/database-yjs/attribution.ts
src/application/types.ts
src/application/database-yjs/row.ts
src/application/services/js-services/http/user-api.ts
src/application/services/js-services/http/workspace-api.ts
src/components/app/layers/AppSyncLayer.tsx
Wire attribution updates into all row-modifying paths (cell edits, relations, new/duplicate rows, row meta, and card moves) while keeping attribution fields read-only.
  • Update cell, relation, and row dispatch hooks to inject currentUser uid and call touchRowAttribution when cells, relations, or row metadata change, including start/end time updates and relation reciprocal/backfill operations.
  • Ensure new rows and duplicated rows initialize attribution via initialDatabaseRow using the acting user, including related-database row creation and database templates.
  • Treat CreatedBy/LastEditedBy as non-editable field types: prevent materializing cells for them (e.g., on type switch, duplication, clear field), and skip attribution updates when clearing attribution fields.
src/application/database-yjs/dispatch.ts
src/application/database-yjs/dispatch/row.ts
src/application/database-yjs/dispatch/cell.ts
src/application/database-yjs/dispatch/relation.ts
src/components/database/components/database-row/DatabaseRowSubDocument.tsx
src/application/database-yjs/database.type.ts
src/application/database-yjs/row.ts
src/application/database-yjs/__tests__/useUpdateCellDispatch.test.tsx
src/application/database-yjs/__tests__/row.test.ts
src/application/database-yjs/__tests__/useNewRowTemplateDispatch.test.tsx
Expose Created By / Last Edited By as first-class database field types integrated into grouping, filtering, sorting, and UI rendering.
  • Extend FieldType enum with CreatedBy/LastEditedBy plus helpers (ATTRIBUTION_FIELD_TYPES, isAttributionFieldType) and update field creation, default names, labels, icons, property menus, and card fields to support them.
  • Add attribution-aware logic to grouping and sorting: groupByIdentifier and sortBy read numeric uids from row metadata, resolve labels via mentionable users, handle legacy rows without attribution, and treat attribution fields as dynamic groupable and text-sortable types.
  • Update filter parsing and UI (advanced filter panel, person filter menu, chip labels) to treat attribution fields like person filters but keyed by canonical uid instead of person_id.
src/application/database-yjs/database.type.ts
src/application/database-yjs/dispatch.ts
src/application/database-yjs/selector.ts
src/application/database-yjs/filter.ts
src/application/database-yjs/group.ts
src/application/database-yjs/sort.ts
src/components/database/components/filters/advanced/FilterPanelRow.tsx
src/components/database/components/filters/filter-menu/FilterMenu.tsx
src/components/database/components/filters/filter-menu/PersonFilterMenu.tsx
src/components/database/components/filters/overview/useFilterChipLabel.ts
src/components/database/components/field/FieldLabel.tsx
src/components/database/components/field/FieldTypeIcon.tsx
src/components/database/components/field/CardField.tsx
src/components/database/components/property/PropertySelectTrigger.tsx
src/application/database-yjs/__tests__/filter.test.ts
src/application/database-yjs/__tests__/sort.test.ts
src/application/database-yjs/__tests__/group.test.ts
Render attribution fields in grid cells, properties, and cards using a dedicated read-only AttributionCell component backed by the mentionable-user cache.
  • Introduce AttributionCell that reads created_by/last_edited_by from row metadata, resolves the corresponding user via useMentionableUsersWithAutoFetch (by uid), and renders an avatar + display name, emitting text to consumers via onTextChange.
  • Wire AttributionCell into generic Cell and Property renderers, and ensure attribution fields are treated as non-editable in field-editing utilities.
  • Enhance mentionable-user caching to support attribution: normalize uid on load, add single-flight refreshes, and force-refresh when a required uid is missing so attribution cells resolve the latest user data instead of stale fallbacks.
src/components/database/components/cell/attribution/AttributionCell.tsx
src/components/database/components/cell/attribution/index.ts
src/components/database/components/cell/Cell.tsx
src/components/database/components/property/Property.tsx
src/components/database/utils/field-editing.ts
src/components/database/components/cell/person/useMentionableUsers.ts
src/application/types.ts
Add multi-user end-to-end and unit tests to validate attribution semantics and legacy behavior.
  • Add Playwright BDD scenario where Nathan creates a row, Eva edits it later, and both sessions validate Created By and Last Edited By values; include workspace switching and database cleanup utilities.
  • Add unit tests for attribution filters, sorts, and groups, including behavior for legacy rows without attribution and for resolved vs uncached user labels.
  • Add row-level tests verifying canonical storage of large numeric uids, empty attribution when actor is missing, and correct attribution for template-created rows; extend existing tests to assert last_edited_by is updated on edits and that remote document updates do not reattribute rows.
playwright/bdd/features/database/row-attribution.feature
playwright/bdd/steps/database-row-attribution.steps.ts
src/application/database-yjs/__tests__/filter.test.ts
src/application/database-yjs/__tests__/sort.test.ts
src/application/database-yjs/__tests__/group.test.ts
src/application/database-yjs/__tests__/row.test.ts
src/components/database/components/database-row/__tests__/DatabaseRowSubDocument.test.tsx
src/application/database-yjs/__tests__/useNewRowTemplateDispatch.test.tsx
src/application/database-yjs/__tests__/useUpdateCellDispatch.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

Copy link
Copy Markdown

🥷 Ninja i18n – 🛎️ Translations need to be updated

Project /project.inlang

lint rule new reports level link
Missing translation 112 warning contribute (via Fink 🐦)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new attribution handling logic (e.g., checks for isAttributionFieldType and touchRowAttribution) is duplicated across multiple dispatch implementations like useMoveCardDispatch in both dispatch.ts and dispatch/row.ts; consider centralizing this behavior to avoid future divergence between grid and card flows.
  • Person-related filter and picker components now support CreatedBy/LastEditedBy by switching to UID-based identifiers, but they still call useMentionableUsersWithAutoFetch without passing the required UID (unlike AttributionCell); you may want to thread the requiredUid through these usages as well to ensure a one-time refresh when a referenced attribution user is missing from the cache.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new attribution handling logic (e.g., checks for isAttributionFieldType and touchRowAttribution) is duplicated across multiple dispatch implementations like useMoveCardDispatch in both dispatch.ts and dispatch/row.ts; consider centralizing this behavior to avoid future divergence between grid and card flows.
- Person-related filter and picker components now support CreatedBy/LastEditedBy by switching to UID-based identifiers, but they still call useMentionableUsersWithAutoFetch without passing the required UID (unlike AttributionCell); you may want to thread the `requiredUid` through these usages as well to ensure a one-time refresh when a referenced attribution user is missing from the cache.

## Individual Comments

### Comment 1
<location path="src/application/database-yjs/filter.ts" line_range="115-119" />
<code_context>
     case FieldType.URL:
     case FieldType.Relation:
     case FieldType.Person:
+    case FieldType.CreatedBy:
+    case FieldType.LastEditedBy:
       group.set(YjsDatabaseKey.content, '');
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against missing condition snapshots when filtering by CreatedBy/LastEditedBy.

In the new CreatedBy/LastEditedBy branch, `snapshot` is assumed to exist:
```ts
const attribute = fieldType === FieldType.CreatedBy ? YjsDatabaseKey.created_by : YjsDatabaseKey.last_edited_by;
const uid = canonicalizeUserUid(snapshot.row.get(attribute));
```
If `getRowConditionSnapshot` returns `undefined` (e.g. row doc missing/not materialized), this will throw. Elsewhere (e.g. grouping) snapshots are accessed via optional chaining.

To align with that and avoid runtime errors:
```ts
const snapshot = getRowConditionSnapshot(rowDocsForConditions[rowId]);
const attribute = fieldType === FieldType.CreatedBy ? YjsDatabaseKey.created_by : YjsDatabaseKey.last_edited_by;
const uid = canonicalizeUserUid(snapshot?.row.get(attribute));
const userIds = uid === null ? [] : [uid];
```
This keeps filtering safe when snapshots are missing or delayed.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +115 to 119
case FieldType.CreatedBy:
case FieldType.LastEditedBy:
try {
const userIds = JSON.parse(value.content) as string[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against missing condition snapshots when filtering by CreatedBy/LastEditedBy.

In the new CreatedBy/LastEditedBy branch, snapshot is assumed to exist:

const attribute = fieldType === FieldType.CreatedBy ? YjsDatabaseKey.created_by : YjsDatabaseKey.last_edited_by;
const uid = canonicalizeUserUid(snapshot.row.get(attribute));

If getRowConditionSnapshot returns undefined (e.g. row doc missing/not materialized), this will throw. Elsewhere (e.g. grouping) snapshots are accessed via optional chaining.

To align with that and avoid runtime errors:

const snapshot = getRowConditionSnapshot(rowDocsForConditions[rowId]);
const attribute = fieldType === FieldType.CreatedBy ? YjsDatabaseKey.created_by : YjsDatabaseKey.last_edited_by;
const uid = canonicalizeUserUid(snapshot?.row.get(attribute));
const userIds = uid === null ? [] : [uid];

This keeps filtering safe when snapshots are missing or delayed.

@appflowy
appflowy merged commit 659f87a into main Aug 18, 2026
16 checks passed
@appflowy
appflowy deleted the codex/database-created-edited-by-web branch August 18, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant