@taskforcehq/taskforce 0.3.301 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
package/dist/core/Taskforce.js
CHANGED
|
@@ -31,7 +31,8 @@ import { normalizePriorityDefinitions as normalizePriorityDefinitionsInternal, n
|
|
|
31
31
|
import { DEFAULT_CATEGORY_LABEL, DEFAULT_CATEGORY_VALUE, DEFAULT_TYPE_VALUE, } from '../shared/taxonomyDefaults.js';
|
|
32
32
|
import { cloneDefaultWorkflowDefinitions, DEFAULT_WORKFLOW_VALUE, } from '../shared/workflowDefaults.js';
|
|
33
33
|
import { getBuiltInTaxonomyLibraryPack, getDefaultStarterTaxonomyLibraryPack, getEmergencyStarterTaxonomyLibraryPack } from '../shared/taxonomyLibrary.js';
|
|
34
|
-
import {
|
|
34
|
+
import { parseTaskReference, } from '../utils/taskReferences.js';
|
|
35
|
+
import { allocateNextLocalTaskReferenceNumber as allocateNextLocalTaskReferenceNumberPolicy, deriveTaskReferenceLabel, allocateNextTaskReferenceNumber as allocateNextTaskReferenceNumberPolicy, getLocalTaskReferenceSequenceValue as getLocalTaskReferenceSequenceValuePolicy, getMaxLocalTaskReferenceNumber as getMaxLocalTaskReferenceNumberPolicy, getMaxTaskReferenceNumber as getMaxTaskReferenceNumberPolicy, getTaskByLocalReferenceNumber as getTaskByLocalReferenceNumberPolicy, getTaskByReferenceNumber as getTaskByReferenceNumberPolicy, getTaskReferenceSequenceValue as getTaskReferenceSequenceValuePolicy, persistLocalTaskReferenceSequence as persistLocalTaskReferenceSequencePolicy, persistTaskReferenceSequence as persistTaskReferenceSequencePolicy, relocateTaskReferenceConflict as relocateTaskReferenceConflictPolicy, reserveTaskReferenceNumber as reserveTaskReferenceNumberPolicy, resolveTaskIdentifier as resolveTaskIdentifierPolicy, } from './taskReferencePolicy.js';
|
|
35
36
|
import { formatInitiativeReference, formatWorkstreamReference, } from '../utils/planningReferences.js';
|
|
36
37
|
import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
|
|
37
38
|
import { getImageReferenceLabel } from '../utils/imageReferences.js';
|
|
@@ -1109,7 +1110,7 @@ export class TaskforceCore {
|
|
|
1109
1110
|
: path.resolve(this.projectRoot, storagePath);
|
|
1110
1111
|
this.dbPath = path.resolve(this.basePath, 'taskforce.db');
|
|
1111
1112
|
this.configFile = path.resolve(this.basePath, 'config.json');
|
|
1112
|
-
this.globalSettingsDbPath = path.resolve(os.homedir(), '.
|
|
1113
|
+
this.globalSettingsDbPath = path.resolve(os.homedir(), '.taskforce', 'taskforce-global.db');
|
|
1113
1114
|
this.ensureDirectories();
|
|
1114
1115
|
this.migrateLegacyGlobalSettingsDbIfNeeded();
|
|
1115
1116
|
// Initialize Database Adapter (SQLite default; Postgres scaffolded)
|
|
@@ -1174,7 +1175,6 @@ export class TaskforceCore {
|
|
|
1174
1175
|
writeGlobalConfig: (config) => this.writeGlobalConfig(config),
|
|
1175
1176
|
});
|
|
1176
1177
|
this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
|
|
1177
|
-
this.planEntitlementService.repairSignupMonetizationSettingsIfNeeded();
|
|
1178
1178
|
this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
|
|
1179
1179
|
this.planEntitlementService.backfillAccountEntitlements();
|
|
1180
1180
|
this.repairDetachedAnnotatedAttachmentSessions();
|
|
@@ -1208,11 +1208,14 @@ export class TaskforceCore {
|
|
|
1208
1208
|
upsertCanonicalEntitlementRecord(input) {
|
|
1209
1209
|
return this.planEntitlementService.upsertCanonicalEntitlementRecord(input);
|
|
1210
1210
|
}
|
|
1211
|
+
transitionCanonicalCommercialState(input) {
|
|
1212
|
+
return this.planEntitlementService.transitionCanonicalCommercialState(input);
|
|
1213
|
+
}
|
|
1211
1214
|
resolveWorkspaceLimitForUser(userId) {
|
|
1212
1215
|
return this.planEntitlementService.resolveWorkspaceLimitForUser(userId);
|
|
1213
1216
|
}
|
|
1214
|
-
|
|
1215
|
-
return this.planEntitlementService.
|
|
1217
|
+
resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
|
|
1218
|
+
return this.planEntitlementService.resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw);
|
|
1216
1219
|
}
|
|
1217
1220
|
createUserBillingRecord(userId, options) {
|
|
1218
1221
|
return this.planEntitlementService.createUserBillingRecord(userId, options);
|
|
@@ -1269,7 +1272,6 @@ export class TaskforceCore {
|
|
|
1269
1272
|
if (fs.existsSync(this.globalSettingsDbPath))
|
|
1270
1273
|
return;
|
|
1271
1274
|
const candidates = [
|
|
1272
|
-
path.resolve(os.homedir(), '.taskforce', 'taskforce-global.db'),
|
|
1273
1275
|
path.resolve(this.basePath, '_global', 'taskforce-global.db')
|
|
1274
1276
|
];
|
|
1275
1277
|
for (const legacyPath of candidates) {
|
|
@@ -1297,6 +1299,7 @@ export class TaskforceCore {
|
|
|
1297
1299
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1298
1300
|
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1299
1301
|
reference_number INTEGER,
|
|
1302
|
+
local_reference_number INTEGER,
|
|
1300
1303
|
title TEXT NOT NULL,
|
|
1301
1304
|
description TEXT,
|
|
1302
1305
|
status TEXT NOT NULL,
|
|
@@ -1331,6 +1334,13 @@ export class TaskforceCore {
|
|
|
1331
1334
|
PRIMARY KEY (tenant_id, workspace_id)
|
|
1332
1335
|
);
|
|
1333
1336
|
|
|
1337
|
+
CREATE TABLE IF NOT EXISTS workspace_local_task_reference_sequences (
|
|
1338
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1339
|
+
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1340
|
+
next_reference_number INTEGER NOT NULL DEFAULT 0,
|
|
1341
|
+
PRIMARY KEY (tenant_id, workspace_id)
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1334
1344
|
CREATE TABLE IF NOT EXISTS initiatives (
|
|
1335
1345
|
id TEXT PRIMARY KEY,
|
|
1336
1346
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
@@ -2590,11 +2600,11 @@ export class TaskforceCore {
|
|
|
2590
2600
|
const config = this.readConfig(workspaceId, userId) || {};
|
|
2591
2601
|
return typeof config.jsonBackupEnabled === 'boolean';
|
|
2592
2602
|
}
|
|
2593
|
-
|
|
2594
|
-
return this.planEntitlementService.
|
|
2603
|
+
getOnboardingPolicy() {
|
|
2604
|
+
return this.planEntitlementService.getOnboardingPolicy();
|
|
2595
2605
|
}
|
|
2596
|
-
|
|
2597
|
-
return this.planEntitlementService.
|
|
2606
|
+
validateOnboardingPolicy(input) {
|
|
2607
|
+
return this.planEntitlementService.validateOnboardingPolicy(input);
|
|
2598
2608
|
}
|
|
2599
2609
|
updateGlobalSettings(settings) {
|
|
2600
2610
|
updateGlobalSettingsService(this.getGlobalSettingsServiceDeps(), settings);
|
|
@@ -2788,10 +2798,12 @@ export class TaskforceCore {
|
|
|
2788
2798
|
const dueDate = this.sanitizeDateOnly(row.due_date);
|
|
2789
2799
|
const scheduledWeekKey = row.scheduled_week_key || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
|
|
2790
2800
|
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
2801
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
2791
2802
|
return {
|
|
2792
2803
|
id: taskId,
|
|
2793
2804
|
referenceNumber,
|
|
2794
|
-
|
|
2805
|
+
localReferenceNumber,
|
|
2806
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
2795
2807
|
title: row.title,
|
|
2796
2808
|
description: row.description,
|
|
2797
2809
|
status: this.normalizeStatusValue(row.status),
|
|
@@ -2879,31 +2891,24 @@ export class TaskforceCore {
|
|
|
2879
2891
|
const normalized = Math.floor(numeric);
|
|
2880
2892
|
return normalized > 0 ? normalized : null;
|
|
2881
2893
|
}
|
|
2894
|
+
getTaskReferencePolicyDeps() {
|
|
2895
|
+
return {
|
|
2896
|
+
db: this.db,
|
|
2897
|
+
tenantId: this.tenantId,
|
|
2898
|
+
normalizeWorkspaceId: this.normalizeWorkspaceId.bind(this),
|
|
2899
|
+
sanitizeTaskReferenceNumber: this.sanitizeTaskReferenceNumber.bind(this),
|
|
2900
|
+
rowToTask: this.rowToTask.bind(this),
|
|
2901
|
+
getTask: this.getTask.bind(this),
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2882
2904
|
getTaskByReferenceNumber(referenceNumber, workspaceId) {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const row = this.db.prepare(`
|
|
2888
|
-
SELECT * FROM tasks
|
|
2889
|
-
WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ?
|
|
2890
|
-
LIMIT 1
|
|
2891
|
-
`).get(this.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
|
|
2892
|
-
if (!row)
|
|
2893
|
-
return null;
|
|
2894
|
-
return this.rowToTask(row);
|
|
2905
|
+
return getTaskByReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), referenceNumber, workspaceId);
|
|
2906
|
+
}
|
|
2907
|
+
getTaskByLocalReferenceNumber(referenceNumber, workspaceId) {
|
|
2908
|
+
return getTaskByLocalReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), referenceNumber, workspaceId);
|
|
2895
2909
|
}
|
|
2896
2910
|
resolveTaskIdentifier(value, workspaceId) {
|
|
2897
|
-
|
|
2898
|
-
if (!normalizedValue)
|
|
2899
|
-
return null;
|
|
2900
|
-
const referenceNumber = parseTaskReference(normalizedValue);
|
|
2901
|
-
if (referenceNumber) {
|
|
2902
|
-
const exactReferenceMatch = this.getTaskByReferenceNumber(referenceNumber, workspaceId);
|
|
2903
|
-
if (exactReferenceMatch)
|
|
2904
|
-
return exactReferenceMatch;
|
|
2905
|
-
}
|
|
2906
|
-
return this.getTask(normalizedValue, workspaceId);
|
|
2911
|
+
return resolveTaskIdentifierPolicy(this.getTaskReferencePolicyDeps(), value, workspaceId);
|
|
2907
2912
|
}
|
|
2908
2913
|
getInitiative(id, workspaceId) {
|
|
2909
2914
|
const normalizedId = String(id || '').trim();
|
|
@@ -3234,42 +3239,35 @@ export class TaskforceCore {
|
|
|
3234
3239
|
});
|
|
3235
3240
|
return this.listChecklistItems(normalizedTaskId, normalizedWorkspaceId);
|
|
3236
3241
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
RETURNING next_reference_number
|
|
3267
|
-
`).get(this.tenantId, normalizedWorkspaceId);
|
|
3268
|
-
const referenceNumber = this.sanitizeTaskReferenceNumber(row?.next_reference_number);
|
|
3269
|
-
if (!referenceNumber) {
|
|
3270
|
-
throw new TaskforceRuleError('Failed to allocate task reference number', 500, 'TASK_REFERENCE_ALLOC_FAILED');
|
|
3271
|
-
}
|
|
3272
|
-
return referenceNumber;
|
|
3242
|
+
getTaskReferenceSequenceValue(workspaceId) {
|
|
3243
|
+
return getTaskReferenceSequenceValuePolicy(this.getTaskReferencePolicyDeps(), workspaceId);
|
|
3244
|
+
}
|
|
3245
|
+
getLocalTaskReferenceSequenceValue(workspaceId) {
|
|
3246
|
+
return getLocalTaskReferenceSequenceValuePolicy(this.getTaskReferencePolicyDeps(), workspaceId);
|
|
3247
|
+
}
|
|
3248
|
+
getMaxTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3249
|
+
return getMaxTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3250
|
+
}
|
|
3251
|
+
getMaxLocalTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3252
|
+
return getMaxLocalTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3253
|
+
}
|
|
3254
|
+
persistTaskReferenceSequence(workspaceId, referenceNumber) {
|
|
3255
|
+
persistTaskReferenceSequencePolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber);
|
|
3256
|
+
}
|
|
3257
|
+
persistLocalTaskReferenceSequence(workspaceId, referenceNumber) {
|
|
3258
|
+
persistLocalTaskReferenceSequencePolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber);
|
|
3259
|
+
}
|
|
3260
|
+
allocateNextTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3261
|
+
return allocateNextTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3262
|
+
}
|
|
3263
|
+
allocateNextLocalTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3264
|
+
return allocateNextLocalTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3265
|
+
}
|
|
3266
|
+
relocateTaskReferenceConflict(workspaceId, referenceNumber, claimantTaskId, updatedAt) {
|
|
3267
|
+
return relocateTaskReferenceConflictPolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber, claimantTaskId, updatedAt);
|
|
3268
|
+
}
|
|
3269
|
+
reserveTaskReferenceNumber(workspaceId, preferredReferenceNumber, currentTaskId) {
|
|
3270
|
+
return reserveTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, preferredReferenceNumber, currentTaskId);
|
|
3273
3271
|
}
|
|
3274
3272
|
reserveGenericReferenceNumber(params) {
|
|
3275
3273
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(params.workspaceId);
|
|
@@ -4060,7 +4058,7 @@ export class TaskforceCore {
|
|
|
4060
4058
|
if (!normalizedUserId || normalizedUserId === 'anonymous')
|
|
4061
4059
|
return null;
|
|
4062
4060
|
const row = this.db.prepare(`
|
|
4063
|
-
SELECT id, email, display_name, system_role, COALESCE(beta_access, 1) AS beta_access
|
|
4061
|
+
SELECT id, email, display_name, avatar_url, system_role, COALESCE(beta_access, 1) AS beta_access
|
|
4064
4062
|
FROM users
|
|
4065
4063
|
WHERE tenant_id = ? AND id = ?
|
|
4066
4064
|
LIMIT 1
|
|
@@ -4071,13 +4069,17 @@ export class TaskforceCore {
|
|
|
4071
4069
|
userId: row.id,
|
|
4072
4070
|
email: row.email,
|
|
4073
4071
|
displayName: row.display_name || null,
|
|
4072
|
+
avatarUrl: row.avatar_url || null,
|
|
4074
4073
|
systemRole: this.normalizeSystemRole(row.system_role, 'user'),
|
|
4075
4074
|
betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access)
|
|
4076
4075
|
};
|
|
4077
4076
|
}
|
|
4078
|
-
|
|
4077
|
+
updateUserProfile(userId, input) {
|
|
4079
4078
|
const normalizedUserId = String(userId || '').trim();
|
|
4080
|
-
const trimmedDisplayName = String(displayName || '').trim();
|
|
4079
|
+
const trimmedDisplayName = String(input.displayName || '').trim();
|
|
4080
|
+
const normalizedAvatarUrl = input.avatarUrl === undefined
|
|
4081
|
+
? undefined
|
|
4082
|
+
: (typeof input.avatarUrl === 'string' && input.avatarUrl.trim().length > 0 ? input.avatarUrl.trim() : null);
|
|
4081
4083
|
if (!normalizedUserId)
|
|
4082
4084
|
return null;
|
|
4083
4085
|
if (!trimmedDisplayName) {
|
|
@@ -4086,15 +4088,145 @@ export class TaskforceCore {
|
|
|
4086
4088
|
if (trimmedDisplayName.length > 120) {
|
|
4087
4089
|
throw new TaskforceRuleError('Display name must be 120 characters or fewer', 400, 'DISPLAY_NAME_TOO_LONG');
|
|
4088
4090
|
}
|
|
4089
|
-
const
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4091
|
+
const now = new Date().toISOString();
|
|
4092
|
+
const result = normalizedAvatarUrl === undefined
|
|
4093
|
+
? this.db.prepare(`
|
|
4094
|
+
UPDATE users
|
|
4095
|
+
SET display_name = ?, updated_at = ?
|
|
4096
|
+
WHERE tenant_id = ? AND id = ?
|
|
4097
|
+
`).run(trimmedDisplayName, now, this.tenantId, normalizedUserId)
|
|
4098
|
+
: this.db.prepare(`
|
|
4099
|
+
UPDATE users
|
|
4100
|
+
SET display_name = ?, avatar_url = ?, updated_at = ?
|
|
4101
|
+
WHERE tenant_id = ? AND id = ?
|
|
4102
|
+
`).run(trimmedDisplayName, normalizedAvatarUrl, now, this.tenantId, normalizedUserId);
|
|
4094
4103
|
if (Number(result?.changes || 0) <= 0)
|
|
4095
4104
|
return null;
|
|
4096
4105
|
return this.resolveUserIdentity(normalizedUserId);
|
|
4097
4106
|
}
|
|
4107
|
+
updateUserDisplayName(userId, displayName) {
|
|
4108
|
+
return this.updateUserProfile(userId, { displayName });
|
|
4109
|
+
}
|
|
4110
|
+
createUserAvatarUploadDraft(input) {
|
|
4111
|
+
const draftId = String(input.draftId || '').trim();
|
|
4112
|
+
const userId = String(input.userId || '').trim();
|
|
4113
|
+
const storageKey = String(input.storageKey || '').trim();
|
|
4114
|
+
const mimeType = String(input.mimeType || 'application/octet-stream').trim() || 'application/octet-stream';
|
|
4115
|
+
const originalFilename = String(input.originalFilename || '').trim() || 'avatar';
|
|
4116
|
+
const sizeBytes = Number.isFinite(Number(input.sizeBytes)) ? Math.max(0, Math.floor(Number(input.sizeBytes))) : 0;
|
|
4117
|
+
const storageProvider = input.storageProvider === 'r2' || input.storageProvider === 'external' ? input.storageProvider : 'local';
|
|
4118
|
+
if (!draftId)
|
|
4119
|
+
throw new TaskforceRuleError('draftId is required', 400, 'AVATAR_DRAFT_ID_REQUIRED');
|
|
4120
|
+
if (!userId)
|
|
4121
|
+
throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
|
|
4122
|
+
if (!storageKey)
|
|
4123
|
+
throw new TaskforceRuleError('storageKey is required', 400, 'AVATAR_STORAGE_KEY_REQUIRED');
|
|
4124
|
+
const now = new Date().toISOString();
|
|
4125
|
+
this.db.prepare(`
|
|
4126
|
+
INSERT INTO user_avatar_upload_drafts (
|
|
4127
|
+
draft_id, tenant_id, user_id, storage_key, mime_type, original_filename, size_bytes, storage_provider,
|
|
4128
|
+
finalized_at, consumed_at, discarded_at, created_at, updated_at
|
|
4129
|
+
)
|
|
4130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?)
|
|
4131
|
+
`).run(draftId, this.tenantId, userId, storageKey, mimeType, originalFilename, sizeBytes, storageProvider, now, now);
|
|
4132
|
+
return this.getUserAvatarUploadDraft(userId, draftId);
|
|
4133
|
+
}
|
|
4134
|
+
getUserAvatarUploadDraft(userId, draftId) {
|
|
4135
|
+
const normalizedUserId = String(userId || '').trim();
|
|
4136
|
+
const normalizedDraftId = String(draftId || '').trim();
|
|
4137
|
+
if (!normalizedUserId || !normalizedDraftId)
|
|
4138
|
+
return null;
|
|
4139
|
+
const row = this.db.prepare(`
|
|
4140
|
+
SELECT draft_id, user_id, storage_key, mime_type, original_filename, size_bytes, storage_provider,
|
|
4141
|
+
finalized_at, consumed_at, discarded_at, created_at, updated_at
|
|
4142
|
+
FROM user_avatar_upload_drafts
|
|
4143
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ?
|
|
4144
|
+
LIMIT 1
|
|
4145
|
+
`).get(this.tenantId, normalizedUserId, normalizedDraftId);
|
|
4146
|
+
if (!row?.draft_id)
|
|
4147
|
+
return null;
|
|
4148
|
+
return {
|
|
4149
|
+
draftId: String(row.draft_id),
|
|
4150
|
+
userId: String(row.user_id || normalizedUserId),
|
|
4151
|
+
storageKey: String(row.storage_key || ''),
|
|
4152
|
+
mimeType: String(row.mime_type || 'application/octet-stream'),
|
|
4153
|
+
originalFilename: String(row.original_filename || 'avatar'),
|
|
4154
|
+
sizeBytes: Number(row.size_bytes || 0),
|
|
4155
|
+
storageProvider: String(row.storage_provider || 'local') === 'r2'
|
|
4156
|
+
? 'r2'
|
|
4157
|
+
: String(row.storage_provider || 'local') === 'external'
|
|
4158
|
+
? 'external'
|
|
4159
|
+
: 'local',
|
|
4160
|
+
finalizedAt: row.finalized_at ? String(row.finalized_at) : null,
|
|
4161
|
+
consumedAt: row.consumed_at ? String(row.consumed_at) : null,
|
|
4162
|
+
discardedAt: row.discarded_at ? String(row.discarded_at) : null,
|
|
4163
|
+
createdAt: String(row.created_at || ''),
|
|
4164
|
+
updatedAt: String(row.updated_at || '')
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
markUserAvatarUploadDraftFinalized(userId, draftId) {
|
|
4168
|
+
const existing = this.getUserAvatarUploadDraft(userId, draftId);
|
|
4169
|
+
if (!existing || existing.discardedAt)
|
|
4170
|
+
return null;
|
|
4171
|
+
const now = new Date().toISOString();
|
|
4172
|
+
this.db.prepare(`
|
|
4173
|
+
UPDATE user_avatar_upload_drafts
|
|
4174
|
+
SET finalized_at = COALESCE(finalized_at, ?), updated_at = ?
|
|
4175
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
4176
|
+
`).run(now, now, this.tenantId, existing.userId, existing.draftId);
|
|
4177
|
+
return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
|
|
4178
|
+
}
|
|
4179
|
+
markUserAvatarUploadDraftConsumed(userId, draftId) {
|
|
4180
|
+
const existing = this.getUserAvatarUploadDraft(userId, draftId);
|
|
4181
|
+
if (!existing || existing.discardedAt)
|
|
4182
|
+
return null;
|
|
4183
|
+
const now = new Date().toISOString();
|
|
4184
|
+
this.db.prepare(`
|
|
4185
|
+
UPDATE user_avatar_upload_drafts
|
|
4186
|
+
SET consumed_at = COALESCE(consumed_at, ?), updated_at = ?
|
|
4187
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
4188
|
+
`).run(now, now, this.tenantId, existing.userId, existing.draftId);
|
|
4189
|
+
return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
|
|
4190
|
+
}
|
|
4191
|
+
discardUserAvatarUploadDraft(userId, draftId) {
|
|
4192
|
+
const existing = this.getUserAvatarUploadDraft(userId, draftId);
|
|
4193
|
+
if (!existing || existing.discardedAt)
|
|
4194
|
+
return existing;
|
|
4195
|
+
const now = new Date().toISOString();
|
|
4196
|
+
this.db.prepare(`
|
|
4197
|
+
UPDATE user_avatar_upload_drafts
|
|
4198
|
+
SET discarded_at = ?, updated_at = ?
|
|
4199
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
4200
|
+
`).run(now, now, this.tenantId, existing.userId, existing.draftId);
|
|
4201
|
+
return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
|
|
4202
|
+
}
|
|
4203
|
+
cleanupStaleUserAvatarUploadDrafts(userId, olderThanIso) {
|
|
4204
|
+
const normalizedUserId = String(userId || '').trim();
|
|
4205
|
+
const cutoff = String(olderThanIso || '').trim();
|
|
4206
|
+
if (!normalizedUserId || !cutoff)
|
|
4207
|
+
return [];
|
|
4208
|
+
const rows = this.db.prepare(`
|
|
4209
|
+
SELECT draft_id
|
|
4210
|
+
FROM user_avatar_upload_drafts
|
|
4211
|
+
WHERE tenant_id = ?
|
|
4212
|
+
AND user_id = ?
|
|
4213
|
+
AND discarded_at IS NULL
|
|
4214
|
+
AND consumed_at IS NULL
|
|
4215
|
+
AND created_at < ?
|
|
4216
|
+
`).all(this.tenantId, normalizedUserId, cutoff);
|
|
4217
|
+
const staleDrafts = rows
|
|
4218
|
+
.map((row) => this.getUserAvatarUploadDraft(normalizedUserId, String(row?.draft_id || '')))
|
|
4219
|
+
.filter((row) => Boolean(row && !row.discardedAt));
|
|
4220
|
+
if (staleDrafts.length === 0)
|
|
4221
|
+
return [];
|
|
4222
|
+
const now = new Date().toISOString();
|
|
4223
|
+
this.db.prepare(`
|
|
4224
|
+
UPDATE user_avatar_upload_drafts
|
|
4225
|
+
SET discarded_at = ?, updated_at = ?
|
|
4226
|
+
WHERE tenant_id = ? AND user_id = ? AND discarded_at IS NULL AND consumed_at IS NULL AND created_at < ?
|
|
4227
|
+
`).run(now, now, this.tenantId, normalizedUserId, cutoff);
|
|
4228
|
+
return staleDrafts;
|
|
4229
|
+
}
|
|
4098
4230
|
getSystemRole(userId) {
|
|
4099
4231
|
const normalizedUserId = String(userId || '').trim();
|
|
4100
4232
|
if (!normalizedUserId || normalizedUserId === 'anonymous')
|
|
@@ -4245,6 +4377,193 @@ export class TaskforceCore {
|
|
|
4245
4377
|
`).run(enabled ? 1 : 0, new Date().toISOString(), this.tenantId, normalizedUserId);
|
|
4246
4378
|
return Number(result?.changes || 0) > 0;
|
|
4247
4379
|
}
|
|
4380
|
+
purgeSystemUser(input) {
|
|
4381
|
+
const targetUserId = String(input.targetUserId || '').trim();
|
|
4382
|
+
const actorUserId = String(input.actorUserId || '').trim();
|
|
4383
|
+
const emailConfirmation = String(input.emailConfirmation || '').trim().toLowerCase();
|
|
4384
|
+
if (!targetUserId) {
|
|
4385
|
+
throw new TaskforceRuleError('targetUserId is required', 400, 'TARGET_USER_ID_REQUIRED');
|
|
4386
|
+
}
|
|
4387
|
+
if (!actorUserId || actorUserId === 'anonymous') {
|
|
4388
|
+
throw new TaskforceRuleError('actorUserId is required', 400, 'ACTOR_USER_ID_REQUIRED');
|
|
4389
|
+
}
|
|
4390
|
+
if (!emailConfirmation) {
|
|
4391
|
+
throw new TaskforceRuleError('Email confirmation is required', 400, 'EMAIL_CONFIRMATION_REQUIRED');
|
|
4392
|
+
}
|
|
4393
|
+
if (targetUserId === actorUserId) {
|
|
4394
|
+
throw new TaskforceRuleError('Cannot purge the current system admin session user', 400, 'CURRENT_SYSTEM_USER_PURGE_FORBIDDEN');
|
|
4395
|
+
}
|
|
4396
|
+
return this.db.transaction(() => {
|
|
4397
|
+
const user = this.db.prepare(`
|
|
4398
|
+
SELECT
|
|
4399
|
+
id,
|
|
4400
|
+
email,
|
|
4401
|
+
COALESCE(system_role, 'user') AS system_role,
|
|
4402
|
+
COALESCE(is_disabled, 0) AS is_disabled
|
|
4403
|
+
FROM users
|
|
4404
|
+
WHERE tenant_id = ? AND id = ?
|
|
4405
|
+
LIMIT 1
|
|
4406
|
+
`).get(this.tenantId, targetUserId);
|
|
4407
|
+
if (!user?.id || !user.email) {
|
|
4408
|
+
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
4409
|
+
}
|
|
4410
|
+
const normalizedEmail = String(user.email).trim().toLowerCase();
|
|
4411
|
+
if (normalizedEmail !== emailConfirmation) {
|
|
4412
|
+
throw new TaskforceRuleError('Email confirmation did not match the target user', 400, 'EMAIL_CONFIRMATION_MISMATCH');
|
|
4413
|
+
}
|
|
4414
|
+
const currentRole = this.normalizeSystemRole(user.system_role, 'user');
|
|
4415
|
+
const isDisabled = Boolean(user.is_disabled);
|
|
4416
|
+
if (currentRole === 'system_admin' && !isDisabled && this.countSystemAdmins() <= 1) {
|
|
4417
|
+
throw new TaskforceRuleError('Cannot purge the last enabled system admin', 400, 'LAST_SYSTEM_ADMIN_REQUIRED');
|
|
4418
|
+
}
|
|
4419
|
+
const elevatedWorkspaceMemberships = this.db.prepare(`
|
|
4420
|
+
SELECT
|
|
4421
|
+
m.workspace_id AS workspace_id,
|
|
4422
|
+
COALESCE(m.role, 'member') AS role,
|
|
4423
|
+
w.name AS workspace_name
|
|
4424
|
+
FROM workspace_memberships m
|
|
4425
|
+
LEFT JOIN workspaces w
|
|
4426
|
+
ON w.tenant_id = m.tenant_id
|
|
4427
|
+
AND w.id = m.workspace_id
|
|
4428
|
+
WHERE m.tenant_id = ?
|
|
4429
|
+
AND m.user_id = ?
|
|
4430
|
+
AND COALESCE(m.status, 'active') = 'active'
|
|
4431
|
+
AND COALESCE(m.role, 'member') IN ('owner', 'admin')
|
|
4432
|
+
ORDER BY COALESCE(w.updated_at, w.created_at) DESC, w.name ASC
|
|
4433
|
+
LIMIT 10
|
|
4434
|
+
`).all(this.tenantId, targetUserId);
|
|
4435
|
+
if (elevatedWorkspaceMemberships.length > 0) {
|
|
4436
|
+
throw new TaskforceRuleError('Cannot purge a user with active owner/admin workspace memberships', 409, 'SYSTEM_USER_PURGE_BLOCKED_ACTIVE_WORKSPACE_ROLE', {
|
|
4437
|
+
memberships: elevatedWorkspaceMemberships.map((membership) => ({
|
|
4438
|
+
workspaceId: membership.workspace_id ? String(membership.workspace_id) : null,
|
|
4439
|
+
workspaceName: membership.workspace_name ? String(membership.workspace_name) : null,
|
|
4440
|
+
role: membership.role ? String(membership.role) : null
|
|
4441
|
+
}))
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
const billingRow = this.db.prepare(`
|
|
4445
|
+
SELECT
|
|
4446
|
+
stripe_customer_id,
|
|
4447
|
+
stripe_subscription_id,
|
|
4448
|
+
stripe_price_id,
|
|
4449
|
+
stripe_status,
|
|
4450
|
+
billing_interval
|
|
4451
|
+
FROM user_billing
|
|
4452
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
4453
|
+
LIMIT 1
|
|
4454
|
+
`).get(this.tenantId, targetUserId);
|
|
4455
|
+
const hasCommercialBillingState = [
|
|
4456
|
+
billingRow?.stripe_customer_id,
|
|
4457
|
+
billingRow?.stripe_subscription_id,
|
|
4458
|
+
billingRow?.stripe_price_id,
|
|
4459
|
+
billingRow?.stripe_status,
|
|
4460
|
+
billingRow?.billing_interval
|
|
4461
|
+
].some((value) => String(value || '').trim().length > 0);
|
|
4462
|
+
if (hasCommercialBillingState) {
|
|
4463
|
+
throw new TaskforceRuleError('Cannot purge a user with active commercial billing state', 409, 'SYSTEM_USER_PURGE_BLOCKED_BILLING_STATE');
|
|
4464
|
+
}
|
|
4465
|
+
const now = new Date().toISOString();
|
|
4466
|
+
const runCount = (sql, ...params) => Number(this.db.prepare(sql).run(...params)?.changes || 0);
|
|
4467
|
+
const unassignedTaskCount = runCount(`
|
|
4468
|
+
UPDATE tasks
|
|
4469
|
+
SET assignee = 'unassigned',
|
|
4470
|
+
updated_at = ?
|
|
4471
|
+
WHERE tenant_id = ?
|
|
4472
|
+
AND assignee = ?
|
|
4473
|
+
AND is_archived = 0
|
|
4474
|
+
`, now, this.tenantId, targetUserId);
|
|
4475
|
+
const clearedInitiativeOwnerCount = runCount(`
|
|
4476
|
+
UPDATE initiatives
|
|
4477
|
+
SET owner_id = NULL,
|
|
4478
|
+
updated_at = ?
|
|
4479
|
+
WHERE tenant_id = ?
|
|
4480
|
+
AND owner_id = ?
|
|
4481
|
+
`, now, this.tenantId, targetUserId);
|
|
4482
|
+
const clearedWorkstreamOwnerCount = runCount(`
|
|
4483
|
+
UPDATE workstreams
|
|
4484
|
+
SET owner_id = NULL,
|
|
4485
|
+
updated_at = ?
|
|
4486
|
+
WHERE tenant_id = ?
|
|
4487
|
+
AND owner_id = ?
|
|
4488
|
+
`, now, this.tenantId, targetUserId);
|
|
4489
|
+
const deletedEntitlementCount = runCount(`
|
|
4490
|
+
DELETE FROM account_entitlements
|
|
4491
|
+
WHERE tenant_id = ?
|
|
4492
|
+
AND account_id = ?
|
|
4493
|
+
`, this.tenantId, targetUserId);
|
|
4494
|
+
const deletedWorkspaceMembershipCount = runCount(`
|
|
4495
|
+
DELETE FROM workspace_memberships
|
|
4496
|
+
WHERE tenant_id = ?
|
|
4497
|
+
AND user_id = ?
|
|
4498
|
+
`, this.tenantId, targetUserId);
|
|
4499
|
+
const deletedAuthTokenCount = runCount(`
|
|
4500
|
+
DELETE FROM auth_tokens
|
|
4501
|
+
WHERE tenant_id = ?
|
|
4502
|
+
AND user_id = ?
|
|
4503
|
+
`, this.tenantId, targetUserId);
|
|
4504
|
+
const deletedAvatarDraftCount = runCount(`
|
|
4505
|
+
DELETE FROM user_avatar_upload_drafts
|
|
4506
|
+
WHERE tenant_id = ?
|
|
4507
|
+
AND user_id = ?
|
|
4508
|
+
`, this.tenantId, targetUserId);
|
|
4509
|
+
const deletedMcpAccessTokenCount = runCount(`
|
|
4510
|
+
DELETE FROM mcp_access_tokens
|
|
4511
|
+
WHERE tenant_id = ?
|
|
4512
|
+
AND user_id = ?
|
|
4513
|
+
`, this.tenantId, targetUserId);
|
|
4514
|
+
const deletedMcpOauthAuthCodeCount = runCount(`
|
|
4515
|
+
DELETE FROM mcp_oauth_auth_codes
|
|
4516
|
+
WHERE tenant_id = ?
|
|
4517
|
+
AND user_id = ?
|
|
4518
|
+
`, this.tenantId, targetUserId);
|
|
4519
|
+
const deletedMcpOauthConnectorGrantCount = runCount(`
|
|
4520
|
+
DELETE FROM mcp_oauth_connector_grants
|
|
4521
|
+
WHERE tenant_id = ?
|
|
4522
|
+
AND user_id = ?
|
|
4523
|
+
`, this.tenantId, targetUserId);
|
|
4524
|
+
const deletedUiStateCount = runCount(`
|
|
4525
|
+
DELETE FROM ui_state
|
|
4526
|
+
WHERE tenant_id = ?
|
|
4527
|
+
AND user_id = ?
|
|
4528
|
+
`, this.tenantId, targetUserId);
|
|
4529
|
+
const deletedUserPreferenceCount = runCount(`
|
|
4530
|
+
DELETE FROM user_preferences
|
|
4531
|
+
WHERE tenant_id = ?
|
|
4532
|
+
AND user_id = ?
|
|
4533
|
+
`, this.tenantId, targetUserId);
|
|
4534
|
+
const deletedUserBillingCount = runCount(`
|
|
4535
|
+
DELETE FROM user_billing
|
|
4536
|
+
WHERE tenant_id = ?
|
|
4537
|
+
AND user_id = ?
|
|
4538
|
+
`, this.tenantId, targetUserId);
|
|
4539
|
+
const deletedUserCount = runCount(`
|
|
4540
|
+
DELETE FROM users
|
|
4541
|
+
WHERE tenant_id = ?
|
|
4542
|
+
AND id = ?
|
|
4543
|
+
`, this.tenantId, targetUserId);
|
|
4544
|
+
if (deletedUserCount !== 1) {
|
|
4545
|
+
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
4546
|
+
}
|
|
4547
|
+
return {
|
|
4548
|
+
targetUserId,
|
|
4549
|
+
email: String(user.email),
|
|
4550
|
+
unassignedTaskCount,
|
|
4551
|
+
clearedInitiativeOwnerCount,
|
|
4552
|
+
clearedWorkstreamOwnerCount,
|
|
4553
|
+
deletedWorkspaceMembershipCount,
|
|
4554
|
+
deletedAuthTokenCount,
|
|
4555
|
+
deletedAvatarDraftCount,
|
|
4556
|
+
deletedMcpAccessTokenCount,
|
|
4557
|
+
deletedMcpOauthAuthCodeCount,
|
|
4558
|
+
deletedMcpOauthConnectorGrantCount,
|
|
4559
|
+
deletedUiStateCount,
|
|
4560
|
+
deletedUserPreferenceCount,
|
|
4561
|
+
deletedEntitlementCount,
|
|
4562
|
+
deletedUserBillingCount,
|
|
4563
|
+
deletedUserCount
|
|
4564
|
+
};
|
|
4565
|
+
})();
|
|
4566
|
+
}
|
|
4248
4567
|
clearActiveAssignmentsForUnavailableActor(actorId, workspaceId, updatedAt) {
|
|
4249
4568
|
const normalizedActorId = String(actorId || '').trim();
|
|
4250
4569
|
const normalizedWorkspaceId = String(workspaceId || 'default').trim() || 'default';
|
|
@@ -5199,7 +5518,20 @@ export class TaskforceCore {
|
|
|
5199
5518
|
const assignFirstSystemAdmin = !this.hasAnySystemAdmin();
|
|
5200
5519
|
const betaAccess = input.betaAccess !== false;
|
|
5201
5520
|
const requestedWorkspaceId = String(input.workspaceId || '').trim();
|
|
5202
|
-
const
|
|
5521
|
+
const onboardingOutcome = this.resolveRegistrationOnboardingOutcome(input.planVersionId);
|
|
5522
|
+
const applyRegistrationCommercialState = (accountId, now) => {
|
|
5523
|
+
this.transitionCanonicalCommercialState({
|
|
5524
|
+
accountId,
|
|
5525
|
+
planVersionId: onboardingOutcome.planVersionId,
|
|
5526
|
+
state: onboardingOutcome.commercialState,
|
|
5527
|
+
effectiveAt: now,
|
|
5528
|
+
billingMirrorPlanId: onboardingOutcome.planId,
|
|
5529
|
+
source: 'registration',
|
|
5530
|
+
updatedBy: 'registration',
|
|
5531
|
+
createdAt: now,
|
|
5532
|
+
updatedAt: now
|
|
5533
|
+
});
|
|
5534
|
+
};
|
|
5203
5535
|
if (!requestedWorkspaceId || requestedWorkspaceId.toLowerCase() === 'default') {
|
|
5204
5536
|
const now = new Date().toISOString();
|
|
5205
5537
|
const userId = this.generateEntityId('user');
|
|
@@ -5208,32 +5540,14 @@ export class TaskforceCore {
|
|
|
5208
5540
|
INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, password_hash, system_role, beta_access, is_disabled, created_at, updated_at)
|
|
5209
5541
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
5210
5542
|
`).run(userId, this.tenantId, email, displayName, 'password', email, hashPassword(password), assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
|
|
5211
|
-
|
|
5212
|
-
this.createUserBillingRecord(userId, {
|
|
5213
|
-
entitlementState: 'pending_plan_selection',
|
|
5214
|
-
planId: null
|
|
5215
|
-
});
|
|
5216
|
-
}
|
|
5217
|
-
if (!registrationPlanAssignment.requiresPlanSelection && registrationPlanAssignment.planVersionId && registrationPlanAssignment.planId) {
|
|
5218
|
-
this.createUserBillingRecord(userId, {
|
|
5219
|
-
entitlementState: 'active',
|
|
5220
|
-
planId: registrationPlanAssignment.planId
|
|
5221
|
-
});
|
|
5222
|
-
this.upsertCanonicalEntitlementRecord({
|
|
5223
|
-
accountId: userId,
|
|
5224
|
-
planVersionId: registrationPlanAssignment.planVersionId,
|
|
5225
|
-
state: 'active',
|
|
5226
|
-
effectiveAt: now,
|
|
5227
|
-
updatedBy: 'registration',
|
|
5228
|
-
createdAt: now,
|
|
5229
|
-
updatedAt: now
|
|
5230
|
-
});
|
|
5231
|
-
}
|
|
5543
|
+
applyRegistrationCommercialState(userId, now);
|
|
5232
5544
|
return {
|
|
5233
5545
|
userId,
|
|
5234
5546
|
workspaceId: null,
|
|
5235
5547
|
role: 'member',
|
|
5236
|
-
planSelectionRequired:
|
|
5548
|
+
planSelectionRequired: onboardingOutcome.requiresPlanSelection,
|
|
5549
|
+
checkoutPending: onboardingOutcome.checkoutPending,
|
|
5550
|
+
commercialState: onboardingOutcome.commercialState
|
|
5237
5551
|
};
|
|
5238
5552
|
}
|
|
5239
5553
|
const workspaceId = requestedWorkspaceId;
|
|
@@ -5251,27 +5565,12 @@ export class TaskforceCore {
|
|
|
5251
5565
|
WHERE tenant_id = ? AND id = ?
|
|
5252
5566
|
`).run(new Date().toISOString(), this.tenantId, created.userId);
|
|
5253
5567
|
}
|
|
5254
|
-
|
|
5255
|
-
this.createUserBillingRecord(created.userId, {
|
|
5256
|
-
entitlementState: 'pending_plan_selection',
|
|
5257
|
-
planId: null
|
|
5258
|
-
});
|
|
5259
|
-
}
|
|
5260
|
-
if (!registrationPlanAssignment.requiresPlanSelection && registrationPlanAssignment.planVersionId && registrationPlanAssignment.planId) {
|
|
5261
|
-
this.createUserBillingRecord(created.userId, {
|
|
5262
|
-
entitlementState: 'active',
|
|
5263
|
-
planId: registrationPlanAssignment.planId
|
|
5264
|
-
});
|
|
5265
|
-
this.upsertCanonicalEntitlementRecord({
|
|
5266
|
-
accountId: created.userId,
|
|
5267
|
-
planVersionId: registrationPlanAssignment.planVersionId,
|
|
5268
|
-
state: 'active',
|
|
5269
|
-
updatedBy: 'registration'
|
|
5270
|
-
});
|
|
5271
|
-
}
|
|
5568
|
+
applyRegistrationCommercialState(created.userId);
|
|
5272
5569
|
return {
|
|
5273
5570
|
...created,
|
|
5274
|
-
planSelectionRequired:
|
|
5571
|
+
planSelectionRequired: onboardingOutcome.requiresPlanSelection,
|
|
5572
|
+
checkoutPending: onboardingOutcome.checkoutPending,
|
|
5573
|
+
commercialState: onboardingOutcome.commercialState
|
|
5275
5574
|
};
|
|
5276
5575
|
}
|
|
5277
5576
|
createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
|
|
@@ -5770,6 +6069,9 @@ export class TaskforceCore {
|
|
|
5770
6069
|
this.assertRequiredTaskTaxonomies(normalizedInput.taxonomies, normalizedWorkspaceId);
|
|
5771
6070
|
const incomingId = String(normalizedInput.id || '').trim();
|
|
5772
6071
|
const incomingReferenceNumber = this.sanitizeTaskReferenceNumber(normalizedInput.referenceNumber);
|
|
6072
|
+
const useProvisionalLocalReference = options?.provisionalLocalReference === true
|
|
6073
|
+
&& options?.allowClientProvidedId !== true
|
|
6074
|
+
&& !incomingReferenceNumber;
|
|
5773
6075
|
if (incomingId && !allowClientProvidedId) {
|
|
5774
6076
|
throw new TaskforceRuleError('Client-supplied task id is not allowed for this operation.', 400, 'TASK_ID_NOT_ALLOWED');
|
|
5775
6077
|
}
|
|
@@ -5777,7 +6079,15 @@ export class TaskforceCore {
|
|
|
5777
6079
|
throw new TaskforceRuleError('Client-supplied task reference number is not allowed for this operation.', 400, 'TASK_REFERENCE_NOT_ALLOWED');
|
|
5778
6080
|
}
|
|
5779
6081
|
const id = incomingId || this.generateId();
|
|
5780
|
-
|
|
6082
|
+
if (incomingReferenceNumber && options?.preferIncomingReferenceNumberOnConflict === true) {
|
|
6083
|
+
this.relocateTaskReferenceConflict(normalizedWorkspaceId, incomingReferenceNumber, id, options?.syncUpdatedAt);
|
|
6084
|
+
}
|
|
6085
|
+
const referenceNumber = useProvisionalLocalReference
|
|
6086
|
+
? null
|
|
6087
|
+
: this.reserveTaskReferenceNumber(normalizedWorkspaceId, incomingReferenceNumber, id);
|
|
6088
|
+
const localReferenceNumber = useProvisionalLocalReference
|
|
6089
|
+
? this.allocateNextLocalTaskReferenceNumber(normalizedWorkspaceId, id)
|
|
6090
|
+
: null;
|
|
5781
6091
|
const normalizedCategory = normalizedInput.category || TaskforceCore.DEFAULT_CATEGORY_VALUE;
|
|
5782
6092
|
const normalizedType = normalizedInput.type || this.resolveDefaultTypeValue(normalizedWorkspaceId);
|
|
5783
6093
|
const createdByInput = String(normalizedInput.createdBy || '').trim() || undefined;
|
|
@@ -5816,7 +6126,8 @@ export class TaskforceCore {
|
|
|
5816
6126
|
const task = {
|
|
5817
6127
|
id,
|
|
5818
6128
|
referenceNumber,
|
|
5819
|
-
|
|
6129
|
+
localReferenceNumber,
|
|
6130
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
5820
6131
|
title: normalizedInput.title || 'Untitled Task',
|
|
5821
6132
|
description: normalizedDescription || null,
|
|
5822
6133
|
category: normalizedCategory,
|
|
@@ -5863,6 +6174,7 @@ export class TaskforceCore {
|
|
|
5863
6174
|
id, tenant_id, title, description, status, priority, complexity, type, category,
|
|
5864
6175
|
workspace_id,
|
|
5865
6176
|
reference_number,
|
|
6177
|
+
local_reference_number,
|
|
5866
6178
|
approach, created_at, updated_at, is_archived, taxonomies, created_by, assignee,
|
|
5867
6179
|
scheduled_date, due_date, scheduled_week_key, order_in_day,
|
|
5868
6180
|
workstream_id
|
|
@@ -5870,6 +6182,7 @@ export class TaskforceCore {
|
|
|
5870
6182
|
@id, @tenantId, @title, @description, @status, @priority, @complexity, @type, @category,
|
|
5871
6183
|
@workspaceId,
|
|
5872
6184
|
@referenceNumber,
|
|
6185
|
+
@localReferenceNumber,
|
|
5873
6186
|
@approach, @createdAt, @updatedAt, 0, @taxonomies, @createdBy, @assignee,
|
|
5874
6187
|
@scheduledDate, @dueDate, @scheduledWeekKey, @orderInDay,
|
|
5875
6188
|
@workstreamId
|
|
@@ -5946,7 +6259,8 @@ export class TaskforceCore {
|
|
|
5946
6259
|
created.push(this.createTask(batch[i], {
|
|
5947
6260
|
persistBackup: false,
|
|
5948
6261
|
workspaceId: options?.workspaceId,
|
|
5949
|
-
allowClientProvidedId: options?.allowClientProvidedId
|
|
6262
|
+
allowClientProvidedId: options?.allowClientProvidedId,
|
|
6263
|
+
provisionalLocalReference: options?.provisionalLocalReference
|
|
5950
6264
|
}));
|
|
5951
6265
|
}
|
|
5952
6266
|
if (validateOnly) {
|
|
@@ -5973,7 +6287,8 @@ export class TaskforceCore {
|
|
|
5973
6287
|
this.createTask(draft, {
|
|
5974
6288
|
persistBackup: false,
|
|
5975
6289
|
workspaceId: options?.workspaceId,
|
|
5976
|
-
allowClientProvidedId: options?.allowClientProvidedId
|
|
6290
|
+
allowClientProvidedId: options?.allowClientProvidedId,
|
|
6291
|
+
provisionalLocalReference: options?.provisionalLocalReference
|
|
5977
6292
|
});
|
|
5978
6293
|
throw validateOnlyRollbackError;
|
|
5979
6294
|
})(input);
|
|
@@ -5982,7 +6297,8 @@ export class TaskforceCore {
|
|
|
5982
6297
|
created.push(this.createTask(input, {
|
|
5983
6298
|
persistBackup: false,
|
|
5984
6299
|
workspaceId: options?.workspaceId,
|
|
5985
|
-
allowClientProvidedId: options?.allowClientProvidedId
|
|
6300
|
+
allowClientProvidedId: options?.allowClientProvidedId,
|
|
6301
|
+
provisionalLocalReference: options?.provisionalLocalReference
|
|
5986
6302
|
}));
|
|
5987
6303
|
}
|
|
5988
6304
|
}
|
|
@@ -6075,7 +6391,19 @@ export class TaskforceCore {
|
|
|
6075
6391
|
}
|
|
6076
6392
|
}
|
|
6077
6393
|
if (Object.prototype.hasOwnProperty.call(patch, 'referenceNumber')) {
|
|
6078
|
-
|
|
6394
|
+
const requestedReferenceNumber = this.sanitizeTaskReferenceNumber(patch.referenceNumber);
|
|
6395
|
+
if (requestedReferenceNumber && options?.preferIncomingReferenceNumberOnConflict === true) {
|
|
6396
|
+
this.relocateTaskReferenceConflict(normalizedWorkspaceId, requestedReferenceNumber, taskId, options?.syncUpdatedAt);
|
|
6397
|
+
}
|
|
6398
|
+
patch.referenceNumber = requestedReferenceNumber
|
|
6399
|
+
? this.reserveTaskReferenceNumber(normalizedWorkspaceId, requestedReferenceNumber, taskId)
|
|
6400
|
+
: requestedReferenceNumber;
|
|
6401
|
+
if (requestedReferenceNumber) {
|
|
6402
|
+
patch.localReferenceNumber = null;
|
|
6403
|
+
}
|
|
6404
|
+
}
|
|
6405
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'localReferenceNumber')) {
|
|
6406
|
+
patch.localReferenceNumber = this.sanitizeTaskReferenceNumber(patch.localReferenceNumber);
|
|
6079
6407
|
}
|
|
6080
6408
|
if (patch.status !== undefined) {
|
|
6081
6409
|
patch.status = this.normalizeStatusValue(patch.status);
|
|
@@ -6093,7 +6421,10 @@ export class TaskforceCore {
|
|
|
6093
6421
|
const updatedTask = {
|
|
6094
6422
|
...existing,
|
|
6095
6423
|
...patch,
|
|
6096
|
-
referenceLabel:
|
|
6424
|
+
referenceLabel: deriveTaskReferenceLabel({
|
|
6425
|
+
referenceNumber: patch.referenceNumber ?? existing.referenceNumber,
|
|
6426
|
+
localReferenceNumber: patch.localReferenceNumber ?? existing.localReferenceNumber
|
|
6427
|
+
}),
|
|
6097
6428
|
updatedAt: options?.syncUpdatedAt || patch.updatedAt || new Date().toISOString()
|
|
6098
6429
|
};
|
|
6099
6430
|
const fields = [];
|
|
@@ -6101,13 +6432,14 @@ export class TaskforceCore {
|
|
|
6101
6432
|
const mappableFields = [
|
|
6102
6433
|
'title', 'description', 'status', 'completedAt', 'priority', 'complexity', 'type', 'category',
|
|
6103
6434
|
'approach', 'canceledReason', 'createdBy', 'assignee', 'scheduledDate', 'dueDate',
|
|
6104
|
-
'scheduledWeekKey', 'orderInDay', 'workstreamId', 'referenceNumber'
|
|
6435
|
+
'scheduledWeekKey', 'orderInDay', 'workstreamId', 'referenceNumber', 'localReferenceNumber'
|
|
6105
6436
|
];
|
|
6106
6437
|
const propertyToColumn = {
|
|
6107
6438
|
canceledReason: 'canceled_reason',
|
|
6108
6439
|
completedAt: 'completed_at',
|
|
6109
6440
|
createdBy: 'created_by',
|
|
6110
6441
|
referenceNumber: 'reference_number',
|
|
6442
|
+
localReferenceNumber: 'local_reference_number',
|
|
6111
6443
|
scheduledDate: 'scheduled_date',
|
|
6112
6444
|
dueDate: 'due_date',
|
|
6113
6445
|
scheduledWeekKey: 'scheduled_week_key',
|