Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c0627958d | ||
|
|
499067ae14 | ||
|
|
ca84d28157 | ||
|
|
d1a4902460 | ||
|
|
89ad87aa64 | ||
|
|
0c5c9c844e | ||
|
|
41571ea377 | ||
|
|
570038ad65 | ||
|
|
88add35f0b | ||
|
|
d74e3fa3b5 | ||
|
|
aad77b5315 | ||
|
|
bba102efe3 |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
|
||||
"env": {}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start development environment (frontend + backend + worker)
|
||||
yarn start
|
||||
|
||||
# Individual package development
|
||||
npx nx start twenty-front # Start frontend dev server
|
||||
npx nx start twenty-server # Start backend server
|
||||
npx nx run twenty-server:worker # Start background worker
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Preferred: run a single test file (fast)
|
||||
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
|
||||
|
||||
# Run all tests for a package
|
||||
npx nx test twenty-front # Frontend unit tests
|
||||
npx nx test twenty-server # Backend unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
|
||||
# To run an indivual test or a pattern of tests, use the following command:
|
||||
cd packages/{workspace} && npx jest "pattern or filename"
|
||||
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front
|
||||
npx nx storybook:test twenty-front
|
||||
|
||||
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Linting (diff with main - fastest, always prefer this)
|
||||
npx nx lint:diff-with-main twenty-front
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
|
||||
|
||||
# Linting (full project - slower, use only when needed)
|
||||
npx nx lint twenty-front
|
||||
npx nx lint twenty-server
|
||||
|
||||
# Type checking
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
|
||||
# Format code
|
||||
npx nx fmt twenty-front
|
||||
npx nx fmt twenty-server
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build packages (twenty-shared must be built first)
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
```bash
|
||||
# Database management
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
|
||||
|
||||
# Generate an instance command (fast or slow)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
|
||||
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Inspect workspace data, metadata, and object definitions while developing
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
### Package Structure
|
||||
```
|
||||
packages/
|
||||
├── twenty-front/ # React frontend application
|
||||
├── twenty-server/ # NestJS backend API
|
||||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-website/ # Next.js documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
```
|
||||
|
||||
### Key Development Principles
|
||||
- **Functional components only** (no class components)
|
||||
- **Named exports only** (no default exports)
|
||||
- **Types over interfaces** (except when extending third-party interfaces)
|
||||
- **String literals over enums** (except for GraphQL enums)
|
||||
- **No 'any' type allowed** — strict TypeScript enforced
|
||||
- **Event handlers preferred over useEffect** for state updates
|
||||
- **Props down, events up** — unidirectional data flow
|
||||
- **Composition over inheritance**
|
||||
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
|
||||
|
||||
### Naming Conventions
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
|
||||
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
|
||||
- **TypeScript generics**: descriptive names (`TData` not `T`)
|
||||
|
||||
### File Structure
|
||||
- Components under 300 lines, services under 500 lines
|
||||
- Components in their own directories with tests and stories
|
||||
- Use `index.ts` barrel exports for clean imports
|
||||
- Import order: external libraries first, then internal (`@/`), then relative
|
||||
|
||||
### Comments
|
||||
- Use short-form comments (`//`), not JSDoc blocks
|
||||
- Explain WHY (business logic), not WHAT
|
||||
- Do not comment obvious code
|
||||
- Multi-line comments use multiple `//` lines, not `/** */`
|
||||
|
||||
### State Management
|
||||
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
|
||||
- GraphQL cache managed by Apollo Client
|
||||
- Use functional state updates: `setState(prev => prev + 1)`
|
||||
|
||||
### Backend Architecture
|
||||
- **NestJS modules** for feature organization
|
||||
- **TypeORM** for database ORM with PostgreSQL
|
||||
- **GraphQL** API with code-first approach
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Upgrade Commands
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
|
||||
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
|
||||
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
|
||||
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
|
||||
- Include both `up` and `down` logic in instance commands
|
||||
- Never delete or rewrite committed instance command `up`/`down` logic
|
||||
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
|
||||
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
|
||||
### Testing Strategy
|
||||
- **Test behavior, not implementation** — focus on user perspective
|
||||
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
|
||||
- Query by user-visible elements (text, roles, labels) over test IDs
|
||||
- Use `@testing-library/user-event` for realistic interactions
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
|
||||
```bash
|
||||
bash packages/twenty-utils/setup-dev-env.sh
|
||||
```
|
||||
|
||||
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
|
||||
|
||||
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
|
||||
- `--down` — stop services
|
||||
- `--reset` — wipe data and restart fresh
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
|
||||
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Detailed development guidelines and best practices
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "github-connector",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
|
||||
+1
@@ -77,6 +77,7 @@ describe('PullRequestReview object', () => {
|
||||
expect(names).toContain('firstSubmittedAt');
|
||||
expect(names).toContain('lastSubmittedAt');
|
||||
expect(names).toContain('eventCount');
|
||||
expect(names).toContain('isSelfReview');
|
||||
expect(names).toContain('reviewer');
|
||||
expect(names).toContain('pullRequest');
|
||||
expect(names).toContain('reviewEvents');
|
||||
|
||||
+6
-1
@@ -298,7 +298,12 @@ const handler = async (
|
||||
const res = await client.query({
|
||||
pullRequestReviews: {
|
||||
__args: {
|
||||
filter: { reviewerId: { eq: contributorId } },
|
||||
filter: {
|
||||
and: [
|
||||
{ reviewerId: { eq: contributorId } },
|
||||
{ isSelfReview: { eq: false } },
|
||||
],
|
||||
},
|
||||
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
|
||||
first: PAGE_SIZE,
|
||||
after: cursor,
|
||||
|
||||
+1
@@ -198,6 +198,7 @@ const handler = async (
|
||||
const res = await client.query({
|
||||
pullRequestReviews: {
|
||||
__args: {
|
||||
filter: { isSelfReview: { eq: false } },
|
||||
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
|
||||
first: PAGE_SIZE,
|
||||
after: cursor,
|
||||
|
||||
+1
@@ -15,5 +15,6 @@ export async function batchUpsertConsolidatedReviews(
|
||||
eventCount: true,
|
||||
reviewerId: true,
|
||||
pullRequestId: true,
|
||||
isSelfReview: true,
|
||||
}) as Promise<PullRequestReviewRow[]>;
|
||||
}
|
||||
|
||||
+11
-2
@@ -23,7 +23,10 @@ type ReviewEventNode = {
|
||||
reviewerId: string | null;
|
||||
pullRequestId: string | null;
|
||||
reviewer: { ghLogin: string | null } | null;
|
||||
pullRequest: { githubNumber: number | null } | null;
|
||||
pullRequest: {
|
||||
githubNumber: number | null;
|
||||
authorId: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type GroupContext = {
|
||||
@@ -31,6 +34,7 @@ type GroupContext = {
|
||||
reviewerId: string | null;
|
||||
prNumber: number | null;
|
||||
reviewerLogin: string | null;
|
||||
prAuthorId: string | null;
|
||||
events: { state: ReviewEventState; submittedAt: string | null }[];
|
||||
};
|
||||
|
||||
@@ -68,7 +72,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
|
||||
reviewerId: true,
|
||||
pullRequestId: true,
|
||||
reviewer: { ghLogin: true },
|
||||
pullRequest: { githubNumber: true },
|
||||
pullRequest: { githubNumber: true, authorId: true },
|
||||
},
|
||||
},
|
||||
pageInfo: { hasNextPage: true, endCursor: true },
|
||||
@@ -95,6 +99,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
|
||||
reviewerId: node.reviewerId,
|
||||
prNumber: node.pullRequest?.githubNumber ?? null,
|
||||
reviewerLogin: node.reviewer?.ghLogin ?? null,
|
||||
prAuthorId: node.pullRequest?.authorId ?? null,
|
||||
events: [],
|
||||
};
|
||||
groups.set(key, group);
|
||||
@@ -105,6 +110,9 @@ const handler = async (_event: RoutePayload<unknown>) => {
|
||||
if (group.prNumber === null && node.pullRequest?.githubNumber != null) {
|
||||
group.prNumber = node.pullRequest.githubNumber;
|
||||
}
|
||||
if (group.prAuthorId === null && node.pullRequest?.authorId) {
|
||||
group.prAuthorId = node.pullRequest.authorId;
|
||||
}
|
||||
group.events.push({
|
||||
state: node.state,
|
||||
submittedAt: node.submittedAt,
|
||||
@@ -123,6 +131,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
|
||||
prNumber: group.prNumber,
|
||||
reviewerLogin: group.reviewerLogin,
|
||||
events: group.events,
|
||||
prAuthorId: group.prAuthorId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
+13
@@ -24,6 +24,9 @@ export const REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
export const PULL_REQUEST_REVIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'b4d61187-1b78-5d6e-9752-79f994ff6d55';
|
||||
|
||||
export const REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'c1f3e8a2-9b5d-4e7f-8a6c-2d4b6f8e1a93';
|
||||
|
||||
enum ReviewState {
|
||||
APPROVED = 'APPROVED',
|
||||
CHANGES_REQUESTED = 'CHANGES_REQUESTED',
|
||||
@@ -116,5 +119,15 @@ export default defineObject({
|
||||
icon: 'IconHash',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
universalIdentifier: REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
name: 'isSelfReview',
|
||||
type: FieldType.BOOLEAN,
|
||||
label: 'Self review',
|
||||
description:
|
||||
'True when the reviewer is the same contributor as the PR author. Self reviews are excluded from review-count aggregations (top reviewers, contributor stats, etc.) so contributors are credited only for reviews on other people\u2019s PRs.',
|
||||
icon: 'IconUserCheck',
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+1
@@ -8,4 +8,5 @@ export type PullRequestReviewRow = {
|
||||
eventCount?: number | null;
|
||||
reviewerId?: string | null;
|
||||
pullRequestId?: string | null;
|
||||
isSelfReview?: boolean | null;
|
||||
};
|
||||
|
||||
+17
@@ -12,6 +12,7 @@ export type ConsolidatedReviewUpsertInput = {
|
||||
eventCount: number;
|
||||
reviewerId: string | null;
|
||||
pullRequestId: string;
|
||||
isSelfReview: boolean;
|
||||
};
|
||||
|
||||
export type BuildConsolidatedRowParams = {
|
||||
@@ -20,8 +21,23 @@ export type BuildConsolidatedRowParams = {
|
||||
prNumber: number | null;
|
||||
reviewerLogin: string | null;
|
||||
events: ReviewEventForConsolidation[];
|
||||
/**
|
||||
* Author of the PR being reviewed. When non-null and equal to `reviewerId`
|
||||
* the consolidated row is flagged as a self-review so downstream
|
||||
* aggregations (top reviewers, contributor stats, etc.) can exclude it.
|
||||
*/
|
||||
prAuthorId?: string | null;
|
||||
};
|
||||
|
||||
export const isSelfReview = (
|
||||
reviewerId: string | null,
|
||||
prAuthorId: string | null | undefined,
|
||||
): boolean =>
|
||||
reviewerId !== null &&
|
||||
prAuthorId !== null &&
|
||||
prAuthorId !== undefined &&
|
||||
reviewerId === prAuthorId;
|
||||
|
||||
export const buildReviewKey = (
|
||||
pullRequestId: string,
|
||||
reviewerId: string | null,
|
||||
@@ -49,5 +65,6 @@ export const buildConsolidatedRow = (
|
||||
eventCount: verdict.eventCount,
|
||||
reviewerId: params.reviewerId,
|
||||
pullRequestId: params.pullRequestId,
|
||||
isSelfReview: isSelfReview(params.reviewerId, params.prAuthorId ?? null),
|
||||
};
|
||||
};
|
||||
|
||||
+66
@@ -8,6 +8,7 @@ import {
|
||||
buildConsolidatedRow,
|
||||
buildReviewKey,
|
||||
buildConsolidatedTitle,
|
||||
isSelfReview,
|
||||
} from 'src/modules/github/pull-request-review/utils/build-consolidated-row';
|
||||
|
||||
const evt = (
|
||||
@@ -140,6 +141,71 @@ describe('buildConsolidatedRow', () => {
|
||||
eventCount: 3,
|
||||
reviewerId: 'rev-2',
|
||||
pullRequestId: 'pr-1',
|
||||
isSelfReview: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('flags isSelfReview when prAuthorId equals reviewerId', () => {
|
||||
const row = buildConsolidatedRow({
|
||||
pullRequestId: 'pr-1',
|
||||
reviewerId: 'alice',
|
||||
prNumber: 7,
|
||||
reviewerLogin: 'alice',
|
||||
prAuthorId: 'alice',
|
||||
events: [evt('COMMENTED', '2025-04-01T10:00:00Z')],
|
||||
});
|
||||
expect(row.isSelfReview).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag isSelfReview when prAuthorId differs from reviewerId', () => {
|
||||
const row = buildConsolidatedRow({
|
||||
pullRequestId: 'pr-1',
|
||||
reviewerId: 'alice',
|
||||
prNumber: 7,
|
||||
reviewerLogin: 'alice',
|
||||
prAuthorId: 'bob',
|
||||
events: [evt('APPROVED', '2025-04-01T10:00:00Z')],
|
||||
});
|
||||
expect(row.isSelfReview).toBe(false);
|
||||
});
|
||||
|
||||
it('does not flag isSelfReview when prAuthorId is missing', () => {
|
||||
const row = buildConsolidatedRow({
|
||||
pullRequestId: 'pr-1',
|
||||
reviewerId: 'alice',
|
||||
prNumber: 7,
|
||||
reviewerLogin: 'alice',
|
||||
events: [evt('APPROVED', '2025-04-01T10:00:00Z')],
|
||||
});
|
||||
expect(row.isSelfReview).toBe(false);
|
||||
});
|
||||
|
||||
it('does not flag isSelfReview when reviewerId is null (ghost reviewer)', () => {
|
||||
const row = buildConsolidatedRow({
|
||||
pullRequestId: 'pr-1',
|
||||
reviewerId: null,
|
||||
prNumber: 7,
|
||||
reviewerLogin: null,
|
||||
prAuthorId: null,
|
||||
events: [evt('COMMENTED', '2025-04-01T10:00:00Z')],
|
||||
});
|
||||
expect(row.isSelfReview).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSelfReview', () => {
|
||||
it('returns true only when both ids are present and equal', () => {
|
||||
expect(isSelfReview('a', 'a')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when ids differ', () => {
|
||||
expect(isSelfReview('a', 'b')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when either side is null/undefined', () => {
|
||||
expect(isSelfReview(null, 'a')).toBe(false);
|
||||
expect(isSelfReview('a', null)).toBe(false);
|
||||
expect(isSelfReview('a', undefined)).toBe(false);
|
||||
expect(isSelfReview(null, null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+9
@@ -110,9 +110,16 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
|
||||
reviewerId: string | null;
|
||||
prNumber: number;
|
||||
reviewerLogin: string | null;
|
||||
prAuthorId: string | null;
|
||||
events: { state: ReviewEventState; submittedAt: string | null }[];
|
||||
};
|
||||
|
||||
const authorIdByPullRequestId = new Map<string, string | null>();
|
||||
for (const pr of prData) {
|
||||
const id = prIdByNumber.get(pr.githubNumber);
|
||||
if (id) authorIdByPullRequestId.set(id, pr.authorId);
|
||||
}
|
||||
|
||||
let skippedReviews = 0;
|
||||
const reviewEventData: ReviewEventInput[] = [];
|
||||
const groups = new Map<GroupKey, GroupContext>();
|
||||
@@ -141,6 +148,7 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
|
||||
reviewerId,
|
||||
prNumber: pr.number,
|
||||
reviewerLogin: review.author?.login ?? null,
|
||||
prAuthorId: authorIdByPullRequestId.get(pullRequestId) ?? null,
|
||||
events: [],
|
||||
};
|
||||
groups.set(key, group);
|
||||
@@ -177,6 +185,7 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
|
||||
prNumber: group.prNumber,
|
||||
reviewerLogin: group.reviewerLogin,
|
||||
events: group.events,
|
||||
prAuthorId: group.prAuthorId,
|
||||
}),
|
||||
);
|
||||
const consolidatedRecords = await timed(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -49,8 +49,6 @@ RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk tw
|
||||
|
||||
FROM common-deps AS twenty-front-build
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
@@ -138,9 +136,6 @@ USER 1000
|
||||
|
||||
FROM twenty-server AS twenty
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
|
||||
|
||||
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
LABEL org.opencontainers.image.description="Twenty image with backend and frontend."
|
||||
@@ -232,7 +227,6 @@ RUN mkdir -p /data/postgres /data/redis /app/packages/twenty-server/.local-stora
|
||||
&& chown -R postgres:postgres /data/postgres \
|
||||
&& chown 1000:1000 /data/redis /app/packages/twenty-server/.local-storage
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
ARG APP_VERSION=0.0.0
|
||||
|
||||
ENV S6_KEEP_ENV=1
|
||||
@@ -241,7 +235,6 @@ ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
|
||||
REDIS_URL=redis://localhost:6379 \
|
||||
STORAGE_TYPE=local \
|
||||
APP_SECRET=twenty-app-dev-secret-not-for-production \
|
||||
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
|
||||
APP_VERSION=$APP_VERSION \
|
||||
NODE_ENV=development \
|
||||
NODE_PORT=2020 \
|
||||
|
||||
@@ -10,6 +10,10 @@ export type SerializedEventData = {
|
||||
pageY?: number;
|
||||
screenX?: number;
|
||||
screenY?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
movementX?: number;
|
||||
movementY?: number;
|
||||
button?: number;
|
||||
buttons?: number;
|
||||
key?: string;
|
||||
|
||||
@@ -81,6 +81,12 @@ const serializeEvent = (event: unknown): SerializedEventData => {
|
||||
if ('pageY' in domEvent) serialized.pageY = domEvent.pageY as number;
|
||||
if ('screenX' in domEvent) serialized.screenX = domEvent.screenX as number;
|
||||
if ('screenY' in domEvent) serialized.screenY = domEvent.screenY as number;
|
||||
if ('offsetX' in domEvent) serialized.offsetX = domEvent.offsetX as number;
|
||||
if ('offsetY' in domEvent) serialized.offsetY = domEvent.offsetY as number;
|
||||
if ('movementX' in domEvent)
|
||||
serialized.movementX = domEvent.movementX as number;
|
||||
if ('movementY' in domEvent)
|
||||
serialized.movementY = domEvent.movementY as number;
|
||||
if ('button' in domEvent) serialized.button = domEvent.button as number;
|
||||
if ('buttons' in domEvent) serialized.buttons = domEvent.buttons as number;
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
|
||||
"build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
|
||||
"build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 npx vite build",
|
||||
"build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build",
|
||||
"start:prod": "NODE_ENV=production npx serve -s build",
|
||||
"tsup": "npx tsup"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -z "$REACT_APP_SERVER_BASE_URL" ]; then
|
||||
echo "Error: REACT_APP_SERVER_BASE_URL is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Injecting runtime environment variables into index.html..."
|
||||
|
||||
CONFIG_BLOCK=$(cat << EOF
|
||||
|
||||
@@ -18,6 +18,4 @@ const getDefaultUrl = () => {
|
||||
};
|
||||
|
||||
export const REACT_APP_SERVER_BASE_URL =
|
||||
window._env_?.REACT_APP_SERVER_BASE_URL ||
|
||||
process.env.REACT_APP_SERVER_BASE_URL ||
|
||||
getDefaultUrl();
|
||||
window._env_?.REACT_APP_SERVER_BASE_URL || getDefaultUrl();
|
||||
|
||||
+2
-1
@@ -62,7 +62,8 @@ export const SettingsAdminAI = () => {
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const isBillingEnabled = billing?.isBillingEnabled ?? false;
|
||||
const hasEnterpriseAccess =
|
||||
isBillingEnabled || currentWorkspace?.hasValidEnterpriseKey === true;
|
||||
isBillingEnabled ||
|
||||
currentWorkspace?.hasValidEnterpriseValidityToken === true;
|
||||
const [usagePeriod, setUsagePeriod] = useState<PeriodPreset>('30d');
|
||||
const periodOptions = getPeriodOptions();
|
||||
const usageDates = getPeriodDates(usagePeriod);
|
||||
|
||||
@@ -7,9 +7,9 @@ import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/Setting
|
||||
import { UsageBreakdownPieSection } from '@/settings/usage/components/UsageBreakdownPieSection';
|
||||
import { UsageByUserTableSection } from '@/settings/usage/components/UsageByUserTableSection';
|
||||
import { UsageDailyChartSection } from '@/settings/usage/components/UsageDailyChartSection';
|
||||
import { UsageSectionSkeleton } from '@/settings/usage/components/UsageSectionSkeleton';
|
||||
import { AI_OPERATION_TYPES } from '@/settings/usage/constants/AiOperationTypes';
|
||||
import { useUsageAnalyticsData } from '@/settings/usage/hooks/useUsageAnalyticsData';
|
||||
import { UsageSectionSkeleton } from '@/settings/usage/components/UsageSectionSkeleton';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
@@ -25,7 +25,8 @@ export const SettingsAiUsageTab = () => {
|
||||
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
|
||||
|
||||
const hasEnterpriseAccess =
|
||||
isBillingEnabled || currentWorkspace?.hasValidEnterpriseKey === true;
|
||||
isBillingEnabled ||
|
||||
currentWorkspace?.hasValidEnterpriseValidityToken === true;
|
||||
|
||||
const shouldSkipQuery = !hasEnterpriseAccess || !isClickHouseConfigured;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
export const mockedApolloClient = new ApolloClient({
|
||||
link: new HttpLink({
|
||||
uri: process.env.REACT_APP_SERVER_BASE_URL + '/metadata',
|
||||
uri: REACT_APP_SERVER_BASE_URL + '/metadata',
|
||||
}),
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
export const mockedApolloCoreClient = new ApolloClient({
|
||||
link: new HttpLink({
|
||||
uri: process.env.REACT_APP_SERVER_BASE_URL + '/graphql',
|
||||
uri: REACT_APP_SERVER_BASE_URL + '/graphql',
|
||||
}),
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, __dirname, '');
|
||||
|
||||
const {
|
||||
REACT_APP_SERVER_BASE_URL,
|
||||
VITE_BUILD_SOURCEMAP,
|
||||
VITE_HOST,
|
||||
SSL_CERT_PATH,
|
||||
@@ -232,11 +231,7 @@ export default defineConfig(({ mode }) => {
|
||||
envPrefix: 'REACT_APP_',
|
||||
|
||||
define: {
|
||||
_env_: {
|
||||
REACT_APP_SERVER_BASE_URL,
|
||||
},
|
||||
'process.env': {
|
||||
REACT_APP_SERVER_BASE_URL,
|
||||
IS_DEBUG_MODE,
|
||||
IS_DEV_ENV: mode === 'development' ? 'true' : 'false',
|
||||
},
|
||||
|
||||
+14
-14
@@ -26,13 +26,13 @@ export class TestFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should escape backslashes in SQL queries 1`] = `
|
||||
{
|
||||
"className": "UpdatePathFastInstanceCommand",
|
||||
"fileName": "2-1-instance-command-fast-1775000000000-update-path.ts",
|
||||
"fileName": "2-2-instance-command-fast-1775000000000-update-path.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000)
|
||||
export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('UPDATE "core"."config" SET "value" = E\\'path\\\\\\\\to\\\\\\\\file\\'');
|
||||
@@ -49,13 +49,13 @@ export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should escape single quotes in SQL queries 1`] = `
|
||||
{
|
||||
"className": "UpdateConfigFastInstanceCommand",
|
||||
"fileName": "2-1-instance-command-fast-1775000000000-update-config.ts",
|
||||
"fileName": "2-2-instance-command-fast-1775000000000-update-config.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000)
|
||||
export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('UPDATE "core"."config" SET "value" = \\'it\\'\\'s done\\'');
|
||||
@@ -72,13 +72,13 @@ export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a migration with a single up/down query 1`] = `
|
||||
{
|
||||
"className": "AddFooColumnFastInstanceCommand",
|
||||
"fileName": "2-1-instance-command-fast-1775000000000-add-foo-column.ts",
|
||||
"fileName": "2-2-instance-command-fast-1775000000000-add-foo-column.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000)
|
||||
export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."user" ADD "foo" varchar');
|
||||
@@ -95,13 +95,13 @@ export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a migration with multiple queries 1`] = `
|
||||
{
|
||||
"className": "CreateTaskTableFastInstanceCommand",
|
||||
"fileName": "2-1-instance-command-fast-1775000000000-create-task-table.ts",
|
||||
"fileName": "2-2-instance-command-fast-1775000000000-create-task-table.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000)
|
||||
export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE TABLE "core"."task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" varchar NOT NULL)');
|
||||
@@ -120,13 +120,13 @@ export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a migration with query parameters 1`] = `
|
||||
{
|
||||
"className": "SeedSettingFastInstanceCommand",
|
||||
"fileName": "2-1-instance-command-fast-1775000000000-seed-setting.ts",
|
||||
"fileName": "2-2-instance-command-fast-1775000000000-seed-setting.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000)
|
||||
export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('INSERT INTO "core"."setting" ("key", "value") VALUES ($1, $2)', ["theme","dark"]);
|
||||
@@ -143,13 +143,13 @@ export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
|
||||
exports[`InstanceCommandGenerationService should generate a slow instance command with populated up/down 1`] = `
|
||||
{
|
||||
"className": "MakeColumnNotNullableSlowInstanceCommand",
|
||||
"fileName": "2-1-instance-command-slow-1775000000000-make-column-not-nullable.ts",
|
||||
"fileName": "2-2-instance-command-slow-1775000000000-make-column-not-nullable.ts",
|
||||
"fileTemplate": "import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000, { type: 'slow' })
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000, { type: 'slow' })
|
||||
export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCommand {
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
// TODO: implement data backfill before the DDL migration
|
||||
@@ -170,13 +170,13 @@ export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCom
|
||||
exports[`InstanceCommandGenerationService should use default migration name in class and file names 1`] = `
|
||||
{
|
||||
"className": "AutoGeneratedFastInstanceCommand",
|
||||
"fileName": "2-1-instance-command-fast-1775000000000-auto-generated.ts",
|
||||
"fileName": "2-2-instance-command-fast-1775000000000-auto-generated.ts",
|
||||
"fileTemplate": "import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.1.0', 1775000000000)
|
||||
@RegisteredInstanceCommand('2.2.0', 1775000000000)
|
||||
export class AutoGeneratedFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."user" ADD "bar" integer');
|
||||
|
||||
+86
-27
@@ -14,6 +14,7 @@ import {
|
||||
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder';
|
||||
import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console';
|
||||
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
|
||||
@@ -22,10 +23,18 @@ import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-
|
||||
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application/application-package/utils/copy-yarn-engine-and-build-dependencies';
|
||||
import type { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
|
||||
import type { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service';
|
||||
import type { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
const LAYER_BUILD_LOCK_TTL_MS = 120_000;
|
||||
const LAYER_BUILD_LOCK_RETRY_MS = 500;
|
||||
const LAYER_BUILD_LOCK_MAX_RETRIES = 240;
|
||||
const LAYER_BUILD_READY_SENTINEL = '.twenty-layer-ready';
|
||||
|
||||
export interface LocalDriverOptions {
|
||||
logicFunctionResourceService: LogicFunctionResourceService;
|
||||
sdkClientArchiveService: SdkClientArchiveService;
|
||||
cacheLockService: CacheLockService;
|
||||
workspaceCacheService: WorkspaceCacheService;
|
||||
}
|
||||
|
||||
const pathExists = async (targetPath: string): Promise<boolean> => {
|
||||
@@ -41,10 +50,14 @@ const pathExists = async (targetPath: string): Promise<boolean> => {
|
||||
export class LocalDriver implements LogicFunctionDriver {
|
||||
private readonly logicFunctionResourceService: LogicFunctionResourceService;
|
||||
private readonly sdkClientArchiveService: SdkClientArchiveService;
|
||||
private readonly cacheLockService: CacheLockService;
|
||||
private readonly workspaceCacheService: WorkspaceCacheService;
|
||||
|
||||
constructor(options: LocalDriverOptions) {
|
||||
this.logicFunctionResourceService = options.logicFunctionResourceService;
|
||||
this.sdkClientArchiveService = options.sdkClientArchiveService;
|
||||
this.cacheLockService = options.cacheLockService;
|
||||
this.workspaceCacheService = options.workspaceCacheService;
|
||||
}
|
||||
|
||||
private getDepsLayerPath(flatApplication: FlatApplication): string {
|
||||
@@ -75,23 +88,40 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
const depsLayerPath = this.getDepsLayerPath(flatApplication);
|
||||
const depsNodeModulesPath = join(depsLayerPath, 'node_modules');
|
||||
const depsReadySentinelPath = join(
|
||||
depsLayerPath,
|
||||
LAYER_BUILD_READY_SENTINEL,
|
||||
);
|
||||
|
||||
const nodeModulesExist = await pathExists(depsNodeModulesPath);
|
||||
|
||||
if (nodeModulesExist) {
|
||||
if (await pathExists(depsReadySentinelPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wipe any partial leftovers from a previously failed build
|
||||
await fs.rm(depsLayerPath, { recursive: true, force: true });
|
||||
const lockKey = `local-driver-deps-layer:${flatApplication.yarnLockChecksum ?? 'default'}`;
|
||||
|
||||
await this.logicFunctionResourceService.copyDependenciesInMemory({
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
inMemoryFolderPath: depsLayerPath,
|
||||
});
|
||||
await copyYarnEngineAndBuildDependencies(depsLayerPath);
|
||||
await this.cacheLockService.withLock(
|
||||
async () => {
|
||||
if (await pathExists(depsReadySentinelPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.rm(depsLayerPath, { recursive: true, force: true });
|
||||
|
||||
await this.logicFunctionResourceService.copyDependenciesInMemory({
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
inMemoryFolderPath: depsLayerPath,
|
||||
});
|
||||
await copyYarnEngineAndBuildDependencies(depsLayerPath);
|
||||
await fs.writeFile(depsReadySentinelPath, '');
|
||||
},
|
||||
lockKey,
|
||||
{
|
||||
ttl: LAYER_BUILD_LOCK_TTL_MS,
|
||||
ms: LAYER_BUILD_LOCK_RETRY_MS,
|
||||
maxRetries: LAYER_BUILD_LOCK_MAX_RETRIES,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureSdkLayer({
|
||||
@@ -106,28 +136,57 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
const sdkNodeModulesPath = join(sdkLayerPath, 'node_modules');
|
||||
const sdkReadySentinelPath = join(sdkLayerPath, LAYER_BUILD_READY_SENTINEL);
|
||||
|
||||
const nodeModulesExist = await pathExists(sdkNodeModulesPath);
|
||||
|
||||
if (nodeModulesExist && !flatApplication.isSdkLayerStale) {
|
||||
if (
|
||||
(await pathExists(sdkReadySentinelPath)) &&
|
||||
!flatApplication.isSdkLayerStale
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.rm(sdkLayerPath, { recursive: true, force: true });
|
||||
const lockKey = `local-driver-sdk-layer:${flatApplication.workspaceId}:${applicationUniversalIdentifier}`;
|
||||
|
||||
const sdkPackagePath = join(sdkNodeModulesPath, 'twenty-client-sdk');
|
||||
await this.cacheLockService.withLock(
|
||||
async () => {
|
||||
const { flatApplicationMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(
|
||||
flatApplication.workspaceId,
|
||||
['flatApplicationMaps'],
|
||||
);
|
||||
const freshFlatApplication =
|
||||
flatApplicationMaps.byId[flatApplication.id];
|
||||
const isStale = freshFlatApplication?.isSdkLayerStale ?? true;
|
||||
|
||||
await this.sdkClientArchiveService.downloadAndExtractToPackage({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath: sdkPackagePath,
|
||||
});
|
||||
if ((await pathExists(sdkReadySentinelPath)) && !isStale) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sdkClientArchiveService.markSdkLayerFresh({
|
||||
applicationId: flatApplication.id,
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
});
|
||||
await fs.rm(sdkLayerPath, { recursive: true, force: true });
|
||||
|
||||
const sdkPackagePath = join(sdkNodeModulesPath, 'twenty-client-sdk');
|
||||
|
||||
await this.sdkClientArchiveService.downloadAndExtractToPackage({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath: sdkPackagePath,
|
||||
});
|
||||
|
||||
await this.sdkClientArchiveService.markSdkLayerFresh({
|
||||
applicationId: flatApplication.id,
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
});
|
||||
|
||||
await fs.writeFile(sdkReadySentinelPath, '');
|
||||
},
|
||||
lockKey,
|
||||
{
|
||||
ttl: LAYER_BUILD_LOCK_TTL_MS,
|
||||
ms: LAYER_BUILD_LOCK_RETRY_MS,
|
||||
maxRetries: LAYER_BUILD_LOCK_MAX_RETRIES,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async transpile({
|
||||
|
||||
+4
@@ -17,6 +17,7 @@ import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionDriver> {
|
||||
@@ -26,6 +27,7 @@ export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionD
|
||||
private readonly logicFunctionResourceService: LogicFunctionResourceService,
|
||||
private readonly sdkClientArchiveService: SdkClientArchiveService,
|
||||
private readonly cacheLockService: CacheLockService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(twentyConfigService, configGroupHashService);
|
||||
}
|
||||
@@ -51,6 +53,8 @@ export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionD
|
||||
return new LocalDriver({
|
||||
logicFunctionResourceService: this.logicFunctionResourceService,
|
||||
sdkClientArchiveService: this.sdkClientArchiveService,
|
||||
cacheLockService: this.cacheLockService,
|
||||
workspaceCacheService: this.workspaceCacheService,
|
||||
});
|
||||
|
||||
case LogicFunctionDriverType.LAMBDA: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LogicFunctionTriggerModule } from 'src/engine/core-modules/logic-functi
|
||||
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
|
||||
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
@@ -21,6 +22,7 @@ export class LogicFunctionModule {
|
||||
LogicFunctionTriggerModule,
|
||||
LogicFunctionExecutorModule,
|
||||
SdkClientModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [LogicFunctionDriverFactory],
|
||||
exports: [
|
||||
|
||||
+1
-1
@@ -8,5 +8,5 @@ export type ToolRetrievalOptions = {
|
||||
// Apply output compaction (strip nulls/empty values) to dispatch results
|
||||
// before returning. Chat enables this to reduce token usage in the
|
||||
// conversation context; MCP and workflow agents leave raw output intact.
|
||||
serializeOutput?: boolean;
|
||||
compactOutput?: boolean;
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { stripEmptyValues } from 'src/engine/core-modules/tool-provider/output-serialization/strip-empty-values.util';
|
||||
import { stripEmptyValues } from 'src/engine/core-modules/tool-provider/output-transforms/strip-empty-values.util';
|
||||
|
||||
describe('stripEmptyValues', () => {
|
||||
it('should remove null values', () => {
|
||||
+10
-10
@@ -7,7 +7,7 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
|
||||
import { type ToolRetrievalOptions } from 'src/engine/core-modules/tool-provider/interfaces/tool-retrieval-options.type';
|
||||
|
||||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-serialization/compact-tool-output.util';
|
||||
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-transforms/compact-tool-output.util';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type LearnToolsAspect } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
|
||||
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
@@ -107,12 +107,12 @@ export class ToolRegistryService {
|
||||
options?: {
|
||||
wrapWithErrorContext?: boolean;
|
||||
includeLoadingMessage?: boolean;
|
||||
serializeOutput?: boolean;
|
||||
compactOutput?: boolean;
|
||||
},
|
||||
): ToolSet {
|
||||
const toolSet: ToolSet = {};
|
||||
const includeLoadingMessage = options?.includeLoadingMessage ?? true;
|
||||
const serializeOutput = options?.serializeOutput ?? false;
|
||||
const compactOutput = options?.compactOutput ?? false;
|
||||
|
||||
for (const descriptor of descriptors) {
|
||||
const baseSchema = descriptor.inputSchema as Record<string, unknown>;
|
||||
@@ -133,7 +133,7 @@ export class ToolRegistryService {
|
||||
context,
|
||||
);
|
||||
|
||||
return serializeOutput
|
||||
return compactOutput
|
||||
? (compactToolOutput(result) as ToolOutput)
|
||||
: result;
|
||||
};
|
||||
@@ -170,7 +170,7 @@ export class ToolRegistryService {
|
||||
context: ToolContext,
|
||||
options?: {
|
||||
includeLoadingMessage?: boolean;
|
||||
serializeOutput?: boolean;
|
||||
compactOutput?: boolean;
|
||||
},
|
||||
): Promise<ToolSet> {
|
||||
const fullContext = this.buildContextFromToolContext(context);
|
||||
@@ -190,7 +190,7 @@ export class ToolRegistryService {
|
||||
|
||||
return this.hydrateToolSet(descriptors, fullContext, {
|
||||
includeLoadingMessage: options?.includeLoadingMessage,
|
||||
serializeOutput: options?.serializeOutput,
|
||||
compactOutput: options?.compactOutput,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ export class ToolRegistryService {
|
||||
toolName: string,
|
||||
args: Record<string, unknown> | undefined,
|
||||
context: ToolContext,
|
||||
options?: { serializeOutput?: boolean },
|
||||
options?: { compactOutput?: boolean },
|
||||
): Promise<ToolOutput> {
|
||||
try {
|
||||
const fullContext = this.buildContextFromToolContext(context);
|
||||
@@ -258,7 +258,7 @@ export class ToolRegistryService {
|
||||
fullContext,
|
||||
);
|
||||
|
||||
return options?.serializeOutput
|
||||
return options?.compactOutput
|
||||
? (compactToolOutput(result) as ToolOutput)
|
||||
: result;
|
||||
} catch (error) {
|
||||
@@ -286,7 +286,7 @@ export class ToolRegistryService {
|
||||
excludeTools,
|
||||
wrapWithErrorContext,
|
||||
includeLoadingMessage,
|
||||
serializeOutput,
|
||||
compactOutput,
|
||||
} = options;
|
||||
const categorySet = categories ? new Set(categories) : undefined;
|
||||
|
||||
@@ -321,7 +321,7 @@ export class ToolRegistryService {
|
||||
const toolSet = this.hydrateToolSet(filteredDescriptors, context, {
|
||||
wrapWithErrorContext,
|
||||
includeLoadingMessage,
|
||||
serializeOutput,
|
||||
compactOutput,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ export const createExecuteToolTool = (
|
||||
context: ToolContext,
|
||||
options?: {
|
||||
excludeTools?: Set<string>;
|
||||
serializeOutput?: boolean;
|
||||
compactOutput?: boolean;
|
||||
},
|
||||
) => ({
|
||||
description:
|
||||
@@ -67,7 +67,7 @@ export const createExecuteToolTool = (
|
||||
}
|
||||
|
||||
return toolRegistry.resolveAndExecute(toolName, args, context, {
|
||||
serializeOutput: options?.serializeOutput,
|
||||
compactOutput: options?.compactOutput,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const TWENTY_CURRENT_VERSION = '2.1.0' as const;
|
||||
export const TWENTY_CURRENT_VERSION = '2.2.0' as const;
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const TWENTY_NEXT_VERSIONS = ['2.2.0'] as const;
|
||||
export const TWENTY_NEXT_VERSIONS = [] as const;
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ export const TWENTY_PREVIOUS_VERSIONS = [
|
||||
'1.22.0',
|
||||
'1.23.0',
|
||||
'2.0.0',
|
||||
'2.1.0',
|
||||
] as const;
|
||||
|
||||
+8
-1
@@ -4,6 +4,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
|
||||
import { formatDateForClickHouse } from 'src/database/clickHouse/clickHouse.util';
|
||||
import { fillUsageTimeSeriesGaps } from 'src/engine/core-modules/usage/utils/fill-usage-time-series-gaps.util';
|
||||
import { toDisplayCredits } from 'src/engine/core-modules/usage/utils/to-display-credits.util';
|
||||
import { toDollars } from 'src/engine/core-modules/usage/utils/to-dollars.util';
|
||||
|
||||
@@ -230,9 +231,15 @@ export class UsageAnalyticsService {
|
||||
},
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
const points = rows.map((row) => ({
|
||||
date: row.date,
|
||||
creditsUsed: row.creditsUsedMicro,
|
||||
}));
|
||||
|
||||
return fillUsageTimeSeriesGaps({
|
||||
rows: points,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { fillUsageTimeSeriesGaps } from 'src/engine/core-modules/usage/utils/fill-usage-time-series-gaps.util';
|
||||
|
||||
describe('fillUsageTimeSeriesGaps', () => {
|
||||
it('should return all-zero series across the period when no rows are returned', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [],
|
||||
periodStart: new Date('2026-04-20T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ date: '2026-04-20', creditsUsed: 0 },
|
||||
{ date: '2026-04-21', creditsUsed: 0 },
|
||||
{ date: '2026-04-22', creditsUsed: 0 },
|
||||
{ date: '2026-04-23', creditsUsed: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fill internal gaps with zero while preserving real values', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [
|
||||
{ date: '2026-04-20', creditsUsed: 100 },
|
||||
{ date: '2026-04-22', creditsUsed: 250 },
|
||||
],
|
||||
periodStart: new Date('2026-04-20T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ date: '2026-04-20', creditsUsed: 100 },
|
||||
{ date: '2026-04-21', creditsUsed: 0 },
|
||||
{ date: '2026-04-22', creditsUsed: 250 },
|
||||
{ date: '2026-04-23', creditsUsed: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should pad trailing dates with zero when data stops before period end (Félix bug)', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [
|
||||
{ date: '2026-04-15', creditsUsed: 50 },
|
||||
{ date: '2026-04-16', creditsUsed: 75 },
|
||||
],
|
||||
periodStart: new Date('2026-04-15T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-20T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ date: '2026-04-15', creditsUsed: 50 },
|
||||
{ date: '2026-04-16', creditsUsed: 75 },
|
||||
{ date: '2026-04-17', creditsUsed: 0 },
|
||||
{ date: '2026-04-18', creditsUsed: 0 },
|
||||
{ date: '2026-04-19', creditsUsed: 0 },
|
||||
{ date: '2026-04-20', creditsUsed: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should pad leading dates with zero when data starts after period start', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [{ date: '2026-04-22', creditsUsed: 99 }],
|
||||
periodStart: new Date('2026-04-20T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ date: '2026-04-20', creditsUsed: 0 },
|
||||
{ date: '2026-04-21', creditsUsed: 0 },
|
||||
{ date: '2026-04-22', creditsUsed: 99 },
|
||||
{ date: '2026-04-23', creditsUsed: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return data unchanged when every day in period is already present', () => {
|
||||
const rows = [
|
||||
{ date: '2026-04-20', creditsUsed: 1 },
|
||||
{ date: '2026-04-21', creditsUsed: 2 },
|
||||
{ date: '2026-04-22', creditsUsed: 3 },
|
||||
];
|
||||
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows,
|
||||
periodStart: new Date('2026-04-20T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-22T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual(rows);
|
||||
});
|
||||
|
||||
it('should return a single entry when period spans one day', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [{ date: '2026-04-23', creditsUsed: 42 }],
|
||||
periodStart: new Date('2026-04-23T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ date: '2026-04-23', creditsUsed: 42 }]);
|
||||
});
|
||||
|
||||
it('should return an empty array when periodStart is after periodEnd', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [],
|
||||
periodStart: new Date('2026-04-25T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-20T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should treat periodEnd as exclusive to match SQL `timestamp < periodEnd` semantics', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [{ date: '2026-04-23', creditsUsed: 10 }],
|
||||
periodStart: new Date('2026-04-22T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-24T00:00:00.000Z'),
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ date: '2026-04-22', creditsUsed: 0 },
|
||||
{ date: '2026-04-23', creditsUsed: 10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return dates in ascending order', () => {
|
||||
const result = fillUsageTimeSeriesGaps({
|
||||
rows: [
|
||||
{ date: '2026-04-22', creditsUsed: 5 },
|
||||
{ date: '2026-04-20', creditsUsed: 1 },
|
||||
],
|
||||
periodStart: new Date('2026-04-20T00:00:00.000Z'),
|
||||
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
|
||||
});
|
||||
|
||||
expect(result.map((point) => point.date)).toEqual([
|
||||
'2026-04-20',
|
||||
'2026-04-21',
|
||||
'2026-04-22',
|
||||
'2026-04-23',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { type Temporal } from 'temporal-polyfill';
|
||||
import {
|
||||
isPlainDateAfter,
|
||||
isPlainDateBeforeOrEqual,
|
||||
parseToPlainDateOrThrow,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type UsageTimeSeriesPoint } from 'src/engine/core-modules/usage/services/usage-analytics.service';
|
||||
|
||||
type FillUsageTimeSeriesGapsParams = {
|
||||
rows: UsageTimeSeriesPoint[];
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
};
|
||||
|
||||
export const fillUsageTimeSeriesGaps = ({
|
||||
rows,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
}: FillUsageTimeSeriesGapsParams): UsageTimeSeriesPoint[] => {
|
||||
const startDate = parseToPlainDateOrThrow(periodStart.toISOString());
|
||||
const lastIncludedInstant = new Date(periodEnd.getTime() - 1);
|
||||
const endDate = parseToPlainDateOrThrow(lastIncludedInstant.toISOString());
|
||||
|
||||
if (isPlainDateAfter(startDate, endDate)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rowsByDate = new Map<string, UsageTimeSeriesPoint>();
|
||||
|
||||
for (const row of rows) {
|
||||
rowsByDate.set(row.date, row);
|
||||
}
|
||||
|
||||
const filled: UsageTimeSeriesPoint[] = [];
|
||||
let currentDateCursor: Temporal.PlainDate = startDate;
|
||||
|
||||
while (isPlainDateBeforeOrEqual(currentDateCursor, endDate)) {
|
||||
const key = currentDateCursor.toString();
|
||||
const existing = rowsByDate.get(key);
|
||||
|
||||
filled.push(existing ?? { date: key, creditsUsed: 0 });
|
||||
currentDateCursor = currentDateCursor.add({ days: 1 });
|
||||
}
|
||||
|
||||
return filled;
|
||||
};
|
||||
+2
-2
@@ -138,7 +138,7 @@ export class ChatExecutionService {
|
||||
const preloadedTools = await this.toolRegistry.getToolsByName(
|
||||
AI_CHAT_TOOL_NAMES_TO_PRELOAD,
|
||||
toolContext,
|
||||
{ serializeOutput: true },
|
||||
{ compactOutput: true },
|
||||
);
|
||||
|
||||
const resolvedModelId = modelId ?? workspace.smartModel;
|
||||
@@ -186,7 +186,7 @@ export class ChatExecutionService {
|
||||
[EXECUTE_TOOL_TOOL_NAME]: createExecuteToolTool(
|
||||
this.toolRegistry,
|
||||
toolContext,
|
||||
{ serializeOutput: true },
|
||||
{ compactOutput: true },
|
||||
),
|
||||
[LOAD_SKILL_TOOL_NAME]: createLoadSkillTool(
|
||||
(skillNames) =>
|
||||
|
||||
@@ -55,6 +55,12 @@
|
||||
"inputCostPerMillionTokens": 5,
|
||||
"outputCostPerMillionTokens": 30,
|
||||
"cachedInputCostPerMillionTokens": 0.5,
|
||||
"longContextCost": {
|
||||
"inputCostPerMillionTokens": 10,
|
||||
"outputCostPerMillionTokens": 45,
|
||||
"thresholdTokens": 200000,
|
||||
"cachedInputCostPerMillionTokens": 1
|
||||
},
|
||||
"contextWindowTokens": 1050000,
|
||||
"maxOutputTokens": 130000,
|
||||
"modalities": ["image", "pdf"],
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`UpgradeSequenceRunnerService — failing sequence (integration) should throw when cursor command is not found in the sequence 1`] = `"Step "RemovedCommand" not found in upgrade sequence. The sequence only covers versions [1.21.0, 1.22.0, 1.23.0, 2.0.0, 2.1.0]. Please upgrade to 1.21.0 first."`;
|
||||
exports[`UpgradeSequenceRunnerService — failing sequence (integration) should throw when cursor command is not found in the sequence 1`] = `"Step "RemovedCommand" not found in upgrade sequence. The sequence only covers versions [1.21.0, 1.22.0, 1.23.0, 2.0.0, 2.1.0, 2.2.0]. Please upgrade to 1.21.0 first."`;
|
||||
|
||||
@@ -559,9 +559,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case 'CURRENCY': {
|
||||
|
||||
Reference in New Issue
Block a user