feat: implement tm list remote (#1185)
This commit is contained in:
@@ -4,3 +4,5 @@
|
||||
*/
|
||||
|
||||
export { TaskService } from './task-service.js';
|
||||
export { OrganizationService } from './organization.service.js';
|
||||
export type { Organization, Brief } from './organization.service.js';
|
||||
|
||||
363
packages/tm-core/src/services/organization.service.ts
Normal file
363
packages/tm-core/src/services/organization.service.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* @fileoverview Organization and Brief management service
|
||||
* Handles fetching and managing organizations and briefs from the API
|
||||
*/
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Database } from '../types/database.types.js';
|
||||
import { TaskMasterError, ERROR_CODES } from '../errors/task-master-error.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
|
||||
/**
|
||||
* Organization data structure
|
||||
*/
|
||||
export interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brief data structure
|
||||
*/
|
||||
export interface Brief {
|
||||
id: string;
|
||||
accountId: string;
|
||||
documentId: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task data structure from the remote database
|
||||
*/
|
||||
export interface RemoteTask {
|
||||
id: string;
|
||||
briefId: string;
|
||||
documentId: string;
|
||||
position: number | null;
|
||||
subtaskPosition: number | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
// Document details from join
|
||||
document?: {
|
||||
id: string;
|
||||
document_name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for managing organizations and briefs
|
||||
*/
|
||||
export class OrganizationService {
|
||||
private logger = getLogger('OrganizationService');
|
||||
|
||||
constructor(private supabaseClient: SupabaseClient<Database>) {}
|
||||
|
||||
/**
|
||||
* Get all organizations for the authenticated user
|
||||
*/
|
||||
async getOrganizations(): Promise<Organization[]> {
|
||||
try {
|
||||
// The user is already authenticated via the Authorization header
|
||||
// Query the user_accounts view/table (filtered by RLS for current user)
|
||||
const { data, error } = await this.supabaseClient
|
||||
.from('user_accounts')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
slug
|
||||
`);
|
||||
|
||||
if (error) {
|
||||
throw new TaskMasterError(
|
||||
`Failed to fetch organizations: ${error.message}`,
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getOrganizations' },
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
this.logger.debug('No organizations found for user');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Map to our Organization interface
|
||||
return data.map((org) => ({
|
||||
id: org.id ?? '',
|
||||
name: org.name ?? '',
|
||||
slug: org.slug ?? org.id ?? '' // Use ID as fallback if slug is null
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
'Failed to fetch organizations',
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getOrganizations' },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific organization by ID
|
||||
*/
|
||||
async getOrganization(orgId: string): Promise<Organization | null> {
|
||||
try {
|
||||
const { data, error } = await this.supabaseClient
|
||||
.from('accounts')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
slug
|
||||
`)
|
||||
.eq('id', orgId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
// No rows found
|
||||
return null;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
`Failed to fetch organization: ${error.message}`,
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getOrganization', orgId },
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountData =
|
||||
data as Database['public']['Tables']['accounts']['Row'];
|
||||
return {
|
||||
id: accountData.id,
|
||||
name: accountData.name,
|
||||
slug: accountData.slug || accountData.id
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
'Failed to fetch organization',
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getOrganization', orgId },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all briefs for a specific organization
|
||||
*/
|
||||
async getBriefs(orgId: string): Promise<Brief[]> {
|
||||
try {
|
||||
const { data, error } = await this.supabaseClient
|
||||
.from('brief')
|
||||
.select(`
|
||||
id,
|
||||
account_id,
|
||||
document_id,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
`)
|
||||
.eq('account_id', orgId);
|
||||
|
||||
if (error) {
|
||||
throw new TaskMasterError(
|
||||
`Failed to fetch briefs: ${error.message}`,
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getBriefs', orgId },
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
this.logger.debug(`No briefs found for organization ${orgId}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Map to our Brief interface
|
||||
return data.map((brief: any) => ({
|
||||
id: brief.id,
|
||||
accountId: brief.account_id,
|
||||
documentId: brief.document_id,
|
||||
status: brief.status,
|
||||
createdAt: brief.created_at,
|
||||
updatedAt: brief.updated_at
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
'Failed to fetch briefs',
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getBriefs', orgId },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific brief by ID
|
||||
*/
|
||||
async getBrief(briefId: string): Promise<Brief | null> {
|
||||
try {
|
||||
const { data, error } = await this.supabaseClient
|
||||
.from('brief')
|
||||
.select(`
|
||||
id,
|
||||
account_id,
|
||||
document_id,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
`)
|
||||
.eq('id', briefId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
// No rows found
|
||||
return null;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
`Failed to fetch brief: ${error.message}`,
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getBrief', briefId },
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const briefData = data as any;
|
||||
return {
|
||||
id: briefData.id,
|
||||
accountId: briefData.account_id,
|
||||
documentId: briefData.document_id,
|
||||
status: briefData.status,
|
||||
createdAt: briefData.created_at,
|
||||
updatedAt: briefData.updated_at
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
'Failed to fetch brief',
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getBrief', briefId },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a user has access to an organization
|
||||
*/
|
||||
async validateOrgAccess(orgId: string): Promise<boolean> {
|
||||
try {
|
||||
const org = await this.getOrganization(orgId);
|
||||
return org !== null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to validate org access: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a user has access to a brief
|
||||
*/
|
||||
async validateBriefAccess(briefId: string): Promise<boolean> {
|
||||
try {
|
||||
const brief = await this.getBrief(briefId);
|
||||
return brief !== null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to validate brief access: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks for a specific brief
|
||||
*/
|
||||
async getTasks(briefId: string): Promise<RemoteTask[]> {
|
||||
try {
|
||||
const { data, error } = await this.supabaseClient
|
||||
.from('tasks')
|
||||
.select(`
|
||||
*,
|
||||
document:document_id (
|
||||
id,
|
||||
document_name,
|
||||
title,
|
||||
description
|
||||
)
|
||||
`)
|
||||
.eq('brief_id', briefId)
|
||||
.order('position', { ascending: true })
|
||||
.order('subtask_position', { ascending: true })
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
throw new TaskMasterError(
|
||||
`Failed to fetch tasks: ${error.message}`,
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getTasks', briefId },
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
this.logger.debug(`No tasks found for brief ${briefId}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Map to our RemoteTask interface
|
||||
return data.map((task: any) => ({
|
||||
id: task.id,
|
||||
briefId: task.brief_id,
|
||||
documentId: task.document_id,
|
||||
position: task.position,
|
||||
subtaskPosition: task.subtask_position,
|
||||
status: task.status,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at,
|
||||
document: task.document
|
||||
? {
|
||||
id: task.document.id,
|
||||
document_name: task.document.document_name,
|
||||
title: task.document.title,
|
||||
description: task.document.description
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TaskMasterError(
|
||||
'Failed to fetch tasks',
|
||||
ERROR_CODES.API_ERROR,
|
||||
{ operation: 'getTasks', briefId },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ export interface TaskListResult {
|
||||
filtered: number;
|
||||
/** The tag these tasks belong to (only present if explicitly provided) */
|
||||
tag?: string;
|
||||
/** Storage type being used - includes 'auto' for automatic detection */
|
||||
storageType: 'file' | 'api' | 'auto';
|
||||
/** Storage type being used */
|
||||
storageType: 'file' | 'api';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +166,7 @@ export class TaskService {
|
||||
byStatus: Record<TaskStatus, number>;
|
||||
withSubtasks: number;
|
||||
blocked: number;
|
||||
storageType: 'file' | 'api' | 'auto';
|
||||
storageType: 'file' | 'api';
|
||||
}> {
|
||||
const result = await this.getTaskList({
|
||||
tag,
|
||||
@@ -334,7 +334,7 @@ export class TaskService {
|
||||
/**
|
||||
* Get current storage type
|
||||
*/
|
||||
getStorageType(): 'file' | 'api' | 'auto' {
|
||||
getStorageType(): 'file' | 'api' {
|
||||
return this.configManager.getStorageConfig().type;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user