Skip to content

Commit 7691137

Browse files
author
Marvin Zhang
committed
refactor: replace createDataSource with getDataSource for singleton database connection management
1 parent 8af5293 commit 7691137

File tree

14 files changed

+239
-130
lines changed

14 files changed

+239
-130
lines changed

packages/core/src/entities/devlog-entry.entity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export class DevlogEntryEntity {
7777
@Column({ type: 'varchar', length: 255, nullable: true })
7878
assignee?: string;
7979

80-
@Column({ type: 'int', nullable: true, name: 'project_id' })
81-
projectId?: number;
80+
@Column({ type: 'int', name: 'project_id' })
81+
projectId!: number;
8282

8383
// Flattened DevlogContext fields (simple strings and arrays)
8484
@Column({ type: 'text', nullable: true, name: 'business_context' })

packages/core/src/services/devlog-service.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {
1717
TimeSeriesStats,
1818
} from '../types/index.js';
1919
import { DevlogDependencyEntity, DevlogEntryEntity, DevlogNoteEntity } from '../entities/index.js';
20-
import { createDataSource } from '../utils/typeorm-config.js';
20+
import { getDataSource } from '../utils/typeorm-config.js';
2121
import { DevlogValidator } from '../validation/devlog-schemas.js';
2222

2323
interface DevlogServiceInstance {
@@ -32,18 +32,29 @@ export class DevlogService {
3232
private devlogRepository: Repository<DevlogEntryEntity>;
3333

3434
private constructor(private projectId?: number) {
35-
this.database = createDataSource({
36-
entities: [DevlogEntryEntity, DevlogNoteEntity, DevlogDependencyEntity],
37-
});
38-
this.devlogRepository = this.database.getRepository(DevlogEntryEntity);
35+
// Database initialization will happen in ensureInitialized()
36+
this.database = null as any; // Temporary placeholder
37+
this.devlogRepository = null as any; // Temporary placeholder
3938
}
4039

4140
/**
4241
* Initialize the database connection if not already initialized
4342
*/
4443
private async ensureInitialized(): Promise<void> {
45-
if (!this.database.isInitialized) {
46-
await this.database.initialize();
44+
try {
45+
if (!this.database || !this.database.isInitialized) {
46+
console.log('[DevlogService] Getting initialized DataSource...');
47+
this.database = await getDataSource();
48+
this.devlogRepository = this.database.getRepository(DevlogEntryEntity);
49+
console.log(
50+
'[DevlogService] DataSource ready with entities:',
51+
this.database.entityMetadatas.length,
52+
);
53+
console.log('[DevlogService] Repository initialized:', !!this.devlogRepository);
54+
}
55+
} catch (error) {
56+
console.error('[DevlogService] Failed to initialize:', error);
57+
throw error;
4758
}
4859
}
4960

@@ -69,7 +80,7 @@ export class DevlogService {
6980

7081
async get(id: DevlogId): Promise<DevlogEntry | null> {
7182
await this.ensureInitialized();
72-
83+
7384
// Validate devlog ID
7485
const idValidation = DevlogValidator.validateDevlogId(id);
7586
if (!idValidation.success) {
@@ -87,7 +98,7 @@ export class DevlogService {
8798

8899
async save(entry: DevlogEntry): Promise<void> {
89100
await this.ensureInitialized();
90-
101+
91102
// Validate devlog entry data
92103
const validation = DevlogValidator.validateDevlogEntry(entry);
93104
if (!validation.success) {
@@ -138,7 +149,7 @@ export class DevlogService {
138149

139150
async delete(id: DevlogId): Promise<void> {
140151
await this.ensureInitialized();
141-
152+
142153
// Validate devlog ID
143154
const idValidation = DevlogValidator.validateDevlogId(id);
144155
if (!idValidation.success) {
@@ -153,7 +164,7 @@ export class DevlogService {
153164

154165
async list(filter?: DevlogFilter): Promise<PaginatedResult<DevlogEntry>> {
155166
await this.ensureInitialized();
156-
167+
157168
// Validate filter if provided
158169
if (filter) {
159170
const filterValidation = DevlogValidator.validateFilter(filter);
@@ -174,7 +185,7 @@ export class DevlogService {
174185

175186
async search(query: string, filter?: DevlogFilter): Promise<PaginatedResult<DevlogEntry>> {
176187
await this.ensureInitialized();
177-
188+
178189
// Validate search query
179190
if (!query || typeof query !== 'string' || query.trim().length === 0) {
180191
throw new Error('Search query is required and must be a non-empty string');
@@ -206,7 +217,7 @@ export class DevlogService {
206217

207218
async getStats(filter?: DevlogFilter): Promise<DevlogStats> {
208219
await this.ensureInitialized();
209-
220+
210221
// Validate filter if provided
211222
if (filter) {
212223
const filterValidation = DevlogValidator.validateFilter(filter);
@@ -295,7 +306,7 @@ export class DevlogService {
295306
request?: TimeSeriesRequest,
296307
): Promise<TimeSeriesStats> {
297308
await this.ensureInitialized();
298-
309+
299310
// Calculate date range
300311
const days = request?.days || 30;
301312
const to = request?.to ? new Date(request.to) : new Date();
@@ -341,7 +352,7 @@ export class DevlogService {
341352

342353
async getNextId(): Promise<DevlogId> {
343354
await this.ensureInitialized();
344-
355+
345356
const result = await this.devlogRepository
346357
.createQueryBuilder('devlog')
347358
.select('MAX(devlog.id)', 'maxId')

packages/core/src/services/project-service.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
import { DataSource, Repository } from 'typeorm';
99
import type { ProjectMetadata } from '../types/project.js';
1010
import { ProjectEntity } from '../entities/project.entity.js';
11-
import { createDataSource } from '../utils/typeorm-config.js';
11+
import { getDataSource } from '../utils/typeorm-config.js';
1212
import { ProjectValidator } from '../validation/project-schemas.js';
1313

1414
export class ProjectService {
1515
private static instance: ProjectService | null = null;
1616
private database: DataSource;
1717
private repository: Repository<ProjectEntity>;
18-
private initPromise: Promise<void> | null = null;
1918

2019
constructor() {
21-
this.database = createDataSource({ entities: [ProjectEntity] });
22-
this.repository = this.database.getRepository(ProjectEntity);
20+
// Database initialization will happen in ensureInitialized()
21+
this.database = null as any; // Temporary placeholder
22+
this.repository = null as any; // Temporary placeholder
2323
}
2424

2525
static getInstance(): ProjectService {
@@ -29,23 +29,28 @@ export class ProjectService {
2929
return ProjectService.instance;
3030
}
3131

32-
async initialize(): Promise<void> {
33-
if (this.initPromise) {
34-
return this.initPromise; // Return existing initialization promise
32+
/**
33+
* Initialize the database connection if not already initialized
34+
*/
35+
private async ensureInitialized(): Promise<void> {
36+
try {
37+
if (!this.database || !this.database.isInitialized) {
38+
console.log('[ProjectService] Getting initialized DataSource...');
39+
this.database = await getDataSource();
40+
this.repository = this.database.getRepository(ProjectEntity);
41+
console.log(
42+
'[ProjectService] DataSource ready with entities:',
43+
this.database.entityMetadatas.length,
44+
);
45+
console.log('[ProjectService] Repository initialized:', !!this.repository);
46+
47+
// Create default project if it doesn't exist
48+
await this.createDefaultProject();
49+
}
50+
} catch (error) {
51+
console.error('[ProjectService] Failed to initialize:', error);
52+
throw error;
3553
}
36-
37-
this.initPromise = this._initialize();
38-
return this.initPromise;
39-
}
40-
41-
private async _initialize(): Promise<void> {
42-
// Initialize the DataSource first
43-
if (!this.database.isInitialized) {
44-
await this.database.initialize();
45-
}
46-
47-
// Create default project if it doesn't exist
48-
await this.createDefaultProject();
4954
}
5055

5156
/**
@@ -72,13 +77,17 @@ export class ProjectService {
7277
}
7378

7479
async list(): Promise<ProjectMetadata[]> {
80+
await this.ensureInitialized(); // Ensure initialization
81+
7582
const entities = await this.repository.find({
7683
order: { lastAccessedAt: 'DESC' },
7784
});
7885
return entities.map((entity) => entity.toProjectMetadata());
7986
}
8087

8188
async get(id: number): Promise<ProjectMetadata | null> {
89+
await this.ensureInitialized(); // Ensure initialization
90+
8291
const entity = await this.repository.findOne({ where: { id } });
8392

8493
if (!entity) {
@@ -95,6 +104,8 @@ export class ProjectService {
95104
async create(
96105
project: Omit<ProjectMetadata, 'id' | 'createdAt' | 'lastAccessedAt'>,
97106
): Promise<ProjectMetadata> {
107+
await this.ensureInitialized(); // Ensure initialization
108+
98109
// Validate input data
99110
const validation = ProjectValidator.validateCreateRequest(project);
100111
if (!validation.success) {
@@ -125,6 +136,8 @@ export class ProjectService {
125136
}
126137

127138
async update(id: number, updates: Partial<ProjectMetadata>): Promise<ProjectMetadata> {
139+
await this.ensureInitialized(); // Ensure initialization
140+
128141
// Validate project ID
129142
const idValidation = ProjectValidator.validateProjectId(id);
130143
if (!idValidation.success) {
@@ -170,6 +183,8 @@ export class ProjectService {
170183
}
171184

172185
async delete(id: number): Promise<void> {
186+
await this.ensureInitialized(); // Ensure initialization
187+
173188
// Validate project ID
174189
const idValidation = ProjectValidator.validateProjectId(id);
175190
if (!idValidation.success) {

packages/core/src/types/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export interface DevlogEntry {
180180
closedAt?: string; // ISO timestamp when status changed to 'done' or 'cancelled'
181181
assignee?: string;
182182
archived?: boolean; // For long-term management and performance
183-
projectId?: number; // Project context for multi-project isolation
183+
projectId: number; // Project context for multi-project isolation - REQUIRED
184184

185185
// Flattened context fields
186186
acceptanceCriteria?: string[];

packages/core/src/utils/typeorm-config.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,49 @@ export interface TypeORMStorageOptions {
3434
ssl?: boolean;
3535
}
3636

37+
// Singleton DataSource instance
38+
let singletonDataSource: DataSource | null = null;
39+
let initializationPromise: Promise<DataSource> | null = null;
40+
41+
/**
42+
* Get or create the singleton DataSource instance
43+
* All services should use this to ensure they share the same database connection
44+
* Handles race conditions by ensuring only one initialization happens
45+
*/
46+
export async function getDataSource(): Promise<DataSource> {
47+
if (singletonDataSource?.isInitialized) {
48+
return singletonDataSource;
49+
}
50+
51+
// If initialization is already in progress, wait for it
52+
if (initializationPromise) {
53+
return initializationPromise;
54+
}
55+
56+
// Start initialization
57+
initializationPromise = (async () => {
58+
if (!singletonDataSource) {
59+
console.log('[DataSource] Creating singleton DataSource instance...');
60+
const options = parseTypeORMConfig();
61+
singletonDataSource = createDataSource({ options });
62+
}
63+
64+
// Initialize the DataSource if not already initialized
65+
if (!singletonDataSource.isInitialized) {
66+
console.log('[DataSource] Initializing singleton DataSource...');
67+
await singletonDataSource.initialize();
68+
console.log(
69+
'[DataSource] Singleton DataSource initialized with entities:',
70+
singletonDataSource.entityMetadatas.length,
71+
);
72+
}
73+
74+
return singletonDataSource;
75+
})();
76+
77+
return initializationPromise;
78+
}
79+
3780
/**
3881
* Create TypeORM DataSource based on storage options
3982
* Uses caching to prevent duplicate connections in development
@@ -63,6 +106,8 @@ export function createDataSource({
63106
logging: options.logging ?? false,
64107
};
65108

109+
console.log('[DataSource] Creating DataSource with', baseConfig.entities?.length, 'entities');
110+
66111
let config: DataSourceOptions;
67112

68113
switch (options.type) {

0 commit comments

Comments
 (0)