Skip to content

Conversation

@AMoreaux
Copy link
Contributor

@AMoreaux AMoreaux commented Nov 7, 2025

Note

Introduce Redis-backed external field storage and per-user lastViewedAt tracking, integrate into query/filter/order pipelines, add migration and metadata support, trigger updates on record view, and switch queue Redis client usage.

  • External Field Storage (Redis):
    • Add storage to fieldMetadata (migration) and propagate through metadata, mocks, builders, and sync utilities; skip non-Postgres fields in schema generation, migrations, selects, and formatting.
    • Implement Redis infrastructure: RedisFieldsDataSource, RedisFieldRepository, RedisStorageDriver, ExternalFieldDrivers provider/token; wire into TwentyORMModule and WorkspaceDatasourceFactory.
    • Extend WorkspaceSelectQueryBuilder to join external fields via CTEs, select aliased values (external_...), and support ordering; update GraphQL parsers to filter/order using external selectors and iterative addOrderBy.
    • Update helpers: buildColumnsToSelect, computeWhereConditionParts, formatData to respect non-Postgres storage.
  • Per-user lastViewedAt:
    • Add standard lastViewedAt field to base/custom objects (virtual, Redis-backed) and expose in entities.
    • On findOne, write view timestamp to Redis; support sorting/selection of this field.
    • Frontend: RecordShowEffect triggers optimistic lastViewedAt set on view; useUpdateOneRecord allows optional input and optimistic-only updates.
  • Analytics:
    • Add Object Record Viewed event and make audit service insert methods async.
  • Admin/Worker Redis client:
    • Replace getQueueClient with getNoevictionClient across health/queue services and tests.
  • Tests:
    • Add GraphQL query parser tests and adjust existing specs for new fields/clients.

Written by Cursor Bugbot for commit 2ef7c9d. This will update automatically on new commits. Configure here.

… synchronization enhancements

- Added `lastViewedAt` as a redis-backed field for better tracking user interactions with the `company` object.
- Implemented Redis support in `workspace-select-query-builder` and supporting ORM logic (e.g., `redisFieldSqlFactory`, `redisFieldsDataSource`, `redisFieldRepository`).
- Enhanced `graphql-query-parser` to compute redis-driven fields dynamically in listings.
- Updated metadata cache to handle synthetic redis-backed metadata fields, avoiding duplication.
- Extended test cases to validate new field integrations and behavior adjustments.
- Refactored `redis-client.service` to support a no-eviction Redis client.
…lastViewedAt` storage in Redis

- Introduced `OBJECT_RECORD_VIEWED_EVENT` for tracking object record views.
- Optimized `lastViewedAt` field handling by removing legacy synthetic metadata injection.
- Refactored frontend `RecordShowPage` with reusable `RecordShowContent` for enhanced component structure.
- Enhanced Redis-backed field storage with accurate timestamp conversion logic.
# Conflicts:
#	packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
#	packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
…t parameters

- Corrected indentation issues in `graphql-query-find-many-resolver.service.ts` for better readability.
- Removed unused boolean parameters in `graphql-query.parser.spec.ts` to maintain cleaner tests.
# Conflicts:
#	packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
#	packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
…nhance GraphQL query parsing

- Removed unused `RecordShowContent` component to streamline frontend logic.
- Improved GraphQL query parser by replacing `orderBy` logic with more extensible `addOrderBy` handling.
- Added support for external storage aliasing in order-by conditions for better compatibility with composite fields.
# Conflicts:
#	packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts
#	packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts
#	packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts
#	packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts
#	packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts
…across services and tests

- Updated Redis client usage to align with the no-eviction policy for improved reliability.
- Adjusted relevant tests and services to reflect the change.
- Added `storage` to `PartialFieldMetadata`.
…SQL expressions

Added logic to dynamically construct order-by clauses based on the expression map, supporting column aliases, property paths, and null handling. Streamlines query customization and improves support for advanced sorting scenarios.
…e order-by alias handling

- Removed unused parameters from `parseSelectedFields` to streamline GraphQL query parsing.
- Enhanced `workspace-select-query-builder` to prioritize alias usage for `external_` fields when building order-by clauses.
…on and streamline query logic

- Deleted `wrapperWithDoubleQuoteWhenUpperCase` utility and its usages to simplify alias handling.
- Removed `lastViewedAt` field from `company.workspace-entity` in favor of centralizing logic in `base.workspace-entity`.
- Adjusted related tests and constants for consistency.
…iasing, and condition generation logic

- Refactored `setFindOptions` in `workspace-select-query-builder` to handle deduplication and parameter preservation efficiently.
- Enhanced alias handling for `external_` fields in ordering and SQL expressions.
- Added optional `selector` support to `computeWhereConditionParts` for improved query flexibility.
- Adjusted Redis field SQL generation for timestamped values.
…query builder and order-by logic

- Updated `workspace-select-query-builder` to use more descriptive
…ed logic

- Eliminated `externalFieldDrivers` across ORM and GraphQL layers to streamline code.
- Removed redundant Redis analytics handling for `Object Record Viewed` events.
- Added `isVirtualField` to metadata settings for improved clarity in field configurations.
- Refactored type definitions to centralize `CommonFieldMetadataSettings`.
- Introduced `lastViewedAt` field in `build-default-fields-for-custom-object` utility to track the last view of a record by user.
…tViewedAt` update logic

- Updated `updateOneRecordInput` to be optional in `useUpdateOneRecord` hook.
- Added `lastViewedAt` field update logic in `RecordShowEffect` for tracking record view timestamps.
@AMoreaux AMoreaux requested a review from FelixMalfait November 7, 2025 16:29
@AMoreaux AMoreaux self-assigned this Nov 7, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 7, 2025

TODOs/FIXMEs:

  • // todo: move this logic to the driver scoped function: packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts

Generated by 🚫 dangerJS against bfb1857

Comment on lines 64 to 66
const { id: _, ...fieldsToSelectWithoutId } = fieldsToSelect;

return fieldsToSelectWithoutId;
Copy link

Choose a reason for hiding this comment

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

Bug: The buildColumnsToSelect function incorrectly removes the id field from the selection object, breaking downstream operations expecting id.
Severity: CRITICAL | Confidence: 1.00

🔍 Detailed Analysis

The buildColumnsToSelect function explicitly removes the id field from the fieldsToSelect object using destructuring const { id: _, ...fieldsToSelectWithoutId } = fieldsToSelect;. This causes downstream functions, such as buildColumnsToReturn and various query runners, to operate without the primary key id, leading to incorrect database select statements and potential data integrity issues. This change directly contradicts existing test expectations where id: true is anticipated in the result.

💡 Suggested Fix

Modify buildColumnsToSelect to retain the id field in the returned object. Remove the destructuring that excludes id to ensure it's always included in the selection.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts#L64-L66

Potential issue: The `buildColumnsToSelect` function explicitly removes the `id` field
from the `fieldsToSelect` object using destructuring `const { id: _,
...fieldsToSelectWithoutId } = fieldsToSelect;`. This causes downstream functions, such
as `buildColumnsToReturn` and various query runners, to operate without the primary key
`id`, leading to incorrect database select statements and potential data integrity
issues. This change directly contradicts existing test expectations where `id: true` is
anticipated in the result.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Greptile Summary

This PR adds external storage infrastructure for field metadata, specifically implementing Redis storage for the lastViewedAt field to track when users last viewed records.

Key Changes:

  • Added storage column to fieldMetadata table (postgres/redis)
  • Implemented Redis storage driver infrastructure with ZSet-based data collection
  • Added lastViewedAt field to custom objects using Redis storage
  • Modified ORM query builder to join external storage data via CTEs
  • Updated frontend to track record views and update lastViewedAt

Critical Issues Found:

  • SQL Injection Vulnerability: UUID values from Redis are directly interpolated into SQL strings without sanitization in RedisFieldSqlFactory.buildZSetDateTimeParts() (line 20-21)
  • Incorrect Join Logic: leftJoin() API usage in WorkspaceSelectQueryBuilder.appendExternalFields() appears incorrect - passing CTE name as property and alias/condition parameters may not work as intended (line 114)
  • React Effect Issues: RecordShowEffect has unstable dependencies in useEffect that could cause infinite loops, and uses effects instead of event handlers per repository guidelines

Additional Concerns:

  • Hardcoded 100-entry limit in Redis driver without explanation
  • No tests for the critical SQL generation logic
  • Missing UUID format validation before SQL interpolation
  • Type casting workarounds in Redis data source service suggest TypeScript issues

Confidence Score: 1/5

  • This PR has critical security vulnerabilities and logic errors that make it unsafe to merge
  • Score of 1 reflects two critical blocking issues: (1) SQL injection vulnerability in Redis field SQL factory where unsanitized UUIDs from Redis are directly interpolated into SQL strings, and (2) incorrect TypeORM leftJoin API usage that will cause join conditions to be ignored, breaking the core functionality. Additionally, the frontend implementation violates repository guidelines with unstable useEffect dependencies that risk infinite loops.
  • Pay immediate attention to redis-field-sql.factory.ts (SQL injection), workspace-select-query-builder.ts (broken join logic), and RecordShowEffect.tsx (infinite loop risk)

Important Files Changed

File Analysis

Filename Score Overview
packages/twenty-server/src/engine/twenty-orm/factories/redis-field-sql.factory.ts 1/5 New factory for Redis field SQL generation with SQL injection vulnerability in UUID interpolation
packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts 1/5 Added external storage support with incorrect leftJoin API usage that may break join conditions
packages/twenty-front/src/modules/object-record/record-show/components/RecordShowEffect.tsx 2/5 Added lastViewedAt tracking with missing useEffect dependencies causing potential infinite loop
packages/twenty-server/src/database/typeorm/core/migrations/common/1760458320000-add-fieldmetadata-storage-column.ts 5/5 Simple migration adding nullable storage column to fieldMetadata table
packages/twenty-server/src/engine/twenty-orm/storage/drivers/redis-storage.driver.ts 3/5 New Redis storage driver implementing data collection from Redis ZSets with hardcoded 100-entry limit

Sequence Diagram

sequenceDiagram
    participant User
    participant RecordShowEffect
    participant useUpdateOneRecord
    participant GraphQL API
    participant WorkspaceSelectQueryBuilder
    participant RedisStorageDriver
    participant RedisFieldsRepository
    participant Redis
    participant PostgreSQL

    User->>RecordShowEffect: Views record page
    RecordShowEffect->>useUpdateOneRecord: updateOneRecord({idToUpdate, optimisticRecord: {lastViewedAt}})
    useUpdateOneRecord->>GraphQL API: updateOneRecord mutation
    GraphQL API->>RedisFieldsRepository: setZSetEntry(key, recordId, timestamp)
    RedisFieldsRepository->>Redis: ZADD key timestamp recordId
    Redis-->>RedisFieldsRepository: OK
    RedisFieldsRepository-->>GraphQL API: Success
    GraphQL API-->>useUpdateOneRecord: Updated record
    useUpdateOneRecord-->>RecordShowEffect: Record updated

    Note over User,PostgreSQL: Querying records with lastViewedAt

    User->>GraphQL API: Query records
    GraphQL API->>WorkspaceSelectQueryBuilder: findMany with external storage
    WorkspaceSelectQueryBuilder->>WorkspaceSelectQueryBuilder: prepareExternalStorage()
    WorkspaceSelectQueryBuilder->>RedisStorageDriver: collectData(userId, workspaceId, objectMetadataId)
    RedisStorageDriver->>RedisFieldsRepository: getZSetEntriesRange(key)
    RedisFieldsRepository->>Redis: ZREVRANGE key 0 -1 WITHSCORES
    Redis-->>RedisFieldsRepository: [recordId1, score1, recordId2, score2...]
    RedisFieldsRepository-->>RedisStorageDriver: [{id, score}]
    RedisStorageDriver->>RedisStorageDriver: buildZSetDateTimeParts(entries.slice(0,100))
    RedisStorageDriver-->>WorkspaceSelectQueryBuilder: {cteSql, joinOn, selectExpr}
    WorkspaceSelectQueryBuilder->>WorkspaceSelectQueryBuilder: addCommonTableExpression(cteSql)
    WorkspaceSelectQueryBuilder->>WorkspaceSelectQueryBuilder: leftJoin(cteName, alias, joinOn)
    WorkspaceSelectQueryBuilder->>PostgreSQL: SELECT with CTE join
    PostgreSQL-->>WorkspaceSelectQueryBuilder: Query results
    WorkspaceSelectQueryBuilder-->>GraphQL API: Records with lastViewedAt
    GraphQL API-->>User: Display records
Loading

72 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 20 to 21
({ id, score }) =>
`('${id}', to_timestamp(${Number(score) || 0} / 1000)::timestamptz)`,
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: SQL injection vulnerability: id from Redis is directly interpolated into SQL without sanitization

The id value from Redis entries is inserted directly into the SQL string using template literals. If malicious data ends up in Redis (e.g., through data corruption or a separate vulnerability), it could execute arbitrary SQL.

Suggested change
({ id, score }) =>
`('${id}', to_timestamp(${Number(score) || 0} / 1000)::timestamptz)`,
({ id, score }) =>
`($${parameterId++}, to_timestamp(${Number(score) || 0} / 1000)::timestamptz)`,

Use parameterized queries instead of string concatenation, or at minimum validate/sanitize the UUID format.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/engine/twenty-orm/factories/redis-field-sql.factory.ts
Line: 20:21

Comment:
**logic:** SQL injection vulnerability: `id` from Redis is directly interpolated into SQL without sanitization

The `id` value from Redis entries is inserted directly into the SQL string using template literals. If malicious data ends up in Redis (e.g., through data corruption or a separate vulnerability), it could execute arbitrary SQL.

```suggestion
        ({ id, score }) =>
          `($${parameterId++}, to_timestamp(${Number(score) || 0} / 1000)::timestamptz)`,
```

Use parameterized queries instead of string concatenation, or at minimum validate/sanitize the UUID format.

How can I resolve this? If you propose a fix, please make it concise.

columnNames: parts.cteColumns,
});

this.leftJoin(parts.cteName, `${field.name}Records`, parts.joinOn);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Incorrect leftJoin API usage causes join condition to be ignored

The TypeORM leftJoin signature is leftJoin(property, alias, condition?). Here you're passing:

  • parts.cteName (the CTE name, not a property)
  • `${field.name}Records` (intended as alias)
  • parts.joinOn (join condition)

But leftJoin expects the second parameter to be the alias and the third to be the condition. Since parts.cteName equals `${field.name}Records`, the alias becomes the join condition string, and the actual join condition is treated as additional ON clause parameters.

This may cause incorrect SQL generation where the join condition "lastViewedAtRecords"."recordId"::uuid = "alias".id::uuid isn't properly applied.

Use leftJoinAndSelect with proper parameters or verify the generated SQL matches expectations in tests.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts
Line: 114:114

Comment:
**logic:** Incorrect `leftJoin` API usage causes join condition to be ignored

The TypeORM `leftJoin` signature is `leftJoin(property, alias, condition?)`. Here you're passing:
- `parts.cteName` (the CTE name, not a property)
- `` `${field.name}Records` `` (intended as alias)  
- `parts.joinOn` (join condition)

But `leftJoin` expects the **second parameter to be the alias** and the **third to be the condition**. Since `parts.cteName` equals `` `${field.name}Records` ``, the alias becomes the join condition string, and the actual join condition is treated as additional ON clause parameters.

This may cause incorrect SQL generation where the join condition `"lastViewedAtRecords"."recordId"::uuid = "alias".id::uuid` isn't properly applied.

Use `leftJoinAndSelect` with proper parameters or verify the generated SQL matches expectations in tests.

How can I resolve this? If you propose a fix, please make it concise.


const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
// Important: inclure lastViewedAt si ce n’est pas déjà dans la signature
Copy link
Contributor

Choose a reason for hiding this comment

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

style: French comment in codebase: "Important: inclure lastViewedAt si ce n'est pas déjà dans la signature"

Suggested change
// Important: inclure lastViewedAt si ce n’est pas déjà dans la signature
// Important: include lastViewedAt if not already in the signature
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-front/src/modules/object-record/record-show/components/RecordShowEffect.tsx
Line: 53:53

Comment:
**style:** French comment in codebase: "Important: inclure lastViewedAt si ce n'est pas déjà dans la signature"

```suggestion
    // Important: include lastViewedAt if not already in the signature
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 50 to 68
useEffect(() => {
if (!loading && isDefined(record)) {
setRecordStore(record);
updateOneRecord({
idToUpdate: recordId,
optimisticRecord: { lastViewedAt: new Date().toISOString() },
});
}
}, [record, setRecordStore, loading]);
}, [record, setRecordStore, loading, updateOneRecord, recordId]);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Missing dependencies in useEffect causes stale closure and infinite update loop risk

The effect depends on updateOneRecord and setRecordStore but these aren't stable references (created from hooks). On every render:

  1. New updateOneRecord function is created
  2. Effect re-runs
  3. Triggers update which may re-render
  4. Cycle repeats

Additionally, per repository rules (.cursor/rules/react-general-guidelines.mdc), effects should be moved to dedicated EffectComponent or use event handlers instead of useEffect for state updates.

Either:

  • Wrap updateOneRecord call in a separate effect component
  • Use useCallback to stabilize the references
  • Move to an event handler pattern instead of effect-based updates
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-front/src/modules/object-record/record-show/components/RecordShowEffect.tsx
Line: 60:68

Comment:
**logic:** Missing dependencies in `useEffect` causes stale closure and infinite update loop risk

The effect depends on `updateOneRecord` and `setRecordStore` but these aren't stable references (created from hooks). On every render:
1. New `updateOneRecord` function is created
2. Effect re-runs
3. Triggers update which may re-render
4. Cycle repeats

Additionally, per repository rules (`.cursor/rules/react-general-guidelines.mdc`), effects should be moved to dedicated `EffectComponent` or use event handlers instead of `useEffect` for state updates.

Either:
- Wrap `updateOneRecord` call in a separate effect component
- Use `useCallback` to stabilize the references
- Move to an event handler pattern instead of effect-based updates

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 7, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:62515

This environment will automatically shut down when the PR is closed or after 5 hours.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on December 15

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.


await refetchAggregateQueries();
const udpatedRecord =
updatedRecord?.data?.[mutationResponseField] ?? null;
Copy link

Choose a reason for hiding this comment

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

Bug: Mismatched Variable, Incorrect Return

Typo in variable name: udpatedRecord should be updatedRecord. This causes the function to return the wrong variable, as udpatedRecord is assigned the mutation result but updatedRecord refers to the promise object from the mutation call, not the actual data.

Fix in Cursor Fix in Web

alias?: string,
queryRunner?: QueryRunner,
): WorkspaceSelectQueryBuilder<U> {
assertIsDefinedOrThrow(alias);
Copy link

Choose a reason for hiding this comment

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

Bug: Default alias generation unexpectedly fails.

assertIsDefinedOrThrow(alias) is called but alias is optional in the method signature. When createQueryBuilder is called without an alias parameter, this will throw an error, breaking existing code that relies on TypeORM's default behavior of auto-generating aliases.

Fix in Cursor Fix in Web

application: {} as ApplicationEntity,
applicationId: faker.string.uuid(),
universalIdentifier: faker.string.uuid(),
storage: 'postgres',
Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised to see storage at that level, shouldn't it be within settings?

RedisFieldsDataSource,
RedisStorageDriver,
RedisFieldSqlFactory,
// External Field Drivers registry provider
Copy link
Member

Choose a reason for hiding this comment

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

Comment probably not needed

TwentyORMGlobalManager,
PgPoolSharedModule,
ScopedWorkspaceContextFactory,
// Export the registry so it can be injected elsewhere
Copy link
Member

Choose a reason for hiding this comment

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

Remove comment? Also is this a standard pattern? Maybe it is!

type UpdateOneRecordArgs<UpdatedObjectRecord> = {
idToUpdate: string;
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
updateOneRecordInput?: Partial<Omit<UpdatedObjectRecord, 'id'>>;
Copy link
Member

Choose a reason for hiding this comment

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

This change seems a bit odd? Why would we be allowed to call updateOneRecord without any update param?

Copy link
Member

Choose a reason for hiding this comment

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

Would it be better to solve at the root instead? Not sure where the root issue comes from?


const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
// Important: inclure lastViewedAt si ce n’est pas déjà dans la signature
Copy link
Member

Choose a reason for hiding this comment

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

French :)


public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ADD COLUMN "storage" character varying`,
Copy link
Member

Choose a reason for hiding this comment

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

I thought this was in settings?

- Introduced `BackfillRecentViewCommand` to create a 'Recently View' system view for objects with `lastViewedAt` field, filtered to the past week.
- Registered the new command in the upgrade module for seamless integration.
- Updated related services and modules to support the new system view creation.
- Optimized query builders by removing unused logic.
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.

3 participants