feat(core): project task-size estimation flag + atomic employee-setting data patch
Merged
EV
10d ago+1100+1100+0123456789012345678901234567890123456789-5-5-0123456789
Why
The owner asked for two gaps the Plane integration (pm.gauzy.co) hit to be closed in Gauzy itself, as fits Gauzy best:
- Per-project "Estimates" switch. The Plane proxy maps Plane estimates onto Gauzy task sizes. Turning a project's estimates off did nothing, because Gauzy had no place to store it.
- Lost preference updates. One person's preferences live in one employee setting.
PUT /employee-settings/:idreplacesdatawholesale, so two saves of different keys lose one change. A sidebar pin and a widget toggle are an example: each reads the setting and writes it back. The proxy queues saves within one process, but with 8 API pods the two requests often land on different pods.
What
1. organization_project.isTaskSizeEstimationEnabled
boolean NOT NULL DEFAULT true: whether the project's tasks are estimated with its task sizes. The default is today's behaviour, so nothing is backfilled.- It is in
IOrganizationProjectSettingand settable through the existingPUT /organization-projects/setting/:id(UpdateProjectSettingDTO,@IsBoolean). - Migration
1791000000747-AddOrganizationProjectTaskSizeEstimation:- Postgres:
ADD COLUMN IF NOT EXISTSunder an advisory lock. - MySQL:
tinyint, under a named lock, with a column check. - SQLite / better-sqlite3:
boolean NOT NULL DEFAULT (1), with a column check.downkeeps the column there, as for the platform's other additive columns. - Re-running is a no-op on every dialect.
- Postgres:
2. PATCH /employee-settings/:id/data
Body: { organizationId, operations: [{ op: 'set' | 'unset', path: string[], value? }] }.
- Atomic. Read and write happen in one transaction, under a row lock (
FOR UPDATE) on Postgres/MySQL; the embedded dialect serializes writers, so there the transaction is the lock. This holds on the TypeORM and the MikroORM path, so concurrent patches apply one after the other on any pod. The TypeORM path saves through the entity subscriber, so SQLite's text serialization still applies. - Path operations, not a JSON merge patch.
setreplaces exactly the addressed subtree, so a client can replace a whole filter object (a merge patch could never clear keys) or change one sidebar item'sis_pinned.- Paths are key lists, so keys with dots are fine.
- Guards:
- prototype keys refused anywhere in a path;
- no path through an array or scalar;
- at most 10 keys deep and 100 operations;
- a
setneeds a value; - only the setting's own employee may patch it, unless the caller has
ALL_ORG_EDITorCHANGE_SELECTED_EMPLOYEE.
- Why not a version column with optimistic retries: every client would need retry logic, and the conflict here is between different keys, which an atomic key-level update settles without one.
Tests
employee-setting-data.utils.spec.ts(12):- set/unset semantics and subtree replacement;
- the input is not mutated and values are copied;
- null replaced on the way; dotted keys;
- array/scalar paths, prototype keys and limits refused.
employee-setting.service.patch-data.spec.ts(7):- two concurrent patches of different keys keep both, under a row-lock stand-in;
FOR UPDATEon Postgres only;- text data on SQLite;
- ownership refusal, and the ALL_ORG_EDIT exception;
- 404 outside the tenant;
- MikroORM
PESSIMISTIC_WRITEplus flush.
organization-project-task-size-estimation-migration.spec.ts(3), on real better-sqlite3:NOT NULL DEFAULT 1, existing and new rows enabled, re-run is a no-op,downkeeps the column.- The
employee-settingandorganization-projectfolders pass: 11 suites, 178 tests (Node 26,--experimental-vm-modules). tsc -p packages/core/tsconfig.lib.json: no errors in changed files.
Consumer
ever-gauzy-plugins-plane uses both:
- the project PATCH
{ estimate: null | id }maps to the flag; - preference saves send path operations.
Against a Gauzy without this PR, the plugin falls back to today's behaviour.
🤖 Generated with Claude Code
Summary by cubic
Adds a per-project task-size estimation flag and an atomic data-patch endpoint for employee settings, covering two gaps the Plane integration hit.
New Features
organization_project.isTaskSizeEstimationEnabled(boolean NOT NULL DEFAULT true) is settable via the existing project settings endpoint; the default keeps today's behavior.- The migration
1791000000747is idempotent on Postgres, MySQL, and SQLite. The MySQL named lock is under 64 characters (the first revision's lock name exceeded MySQL's limit); advisory locks guard concurrent runs where DDL isn't transactional.
Bug Fixes
PATCH /employee-settings/:id/dataapplies path-basedset/unsetoperations in one transaction under a row lock, so two concurrent patches of different keys both survive. On the embedded dialect, patch transactions are queued per process (a single connection cannot run two at once); a production-wiring test on SQLite covers this.- A
setmay now store an explicitnull, and path traversal reads own keys only, refusing prototype keys and paths through arrays or scalars, with depth and operation limits. - The OpenAPI schema documents the patched
valueas any JSON value: object, array, string, number, boolean, or null. - Only the setting's own employee may patch it, unless permissions allow otherwise.
- The apply function is the last guard that also refuses missing operations and null/undefined operations, with tests covering those cases.