@tpitre/story-ui 3.1.0 → 3.2.0

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.
@@ -0,0 +1,240 @@
1
+ import pg from 'pg';
2
+ const { Pool } = pg;
3
+ /**
4
+ * PostgreSQL Story Service
5
+ * Persistent story storage using PostgreSQL database
6
+ * Designed for Railway PostgreSQL deployments
7
+ */
8
+ export class PostgresStoryService {
9
+ constructor(connectionString) {
10
+ this.initialized = false;
11
+ this.pool = new Pool({
12
+ connectionString,
13
+ ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
14
+ max: 10,
15
+ idleTimeoutMillis: 30000,
16
+ connectionTimeoutMillis: 10000,
17
+ });
18
+ }
19
+ /**
20
+ * Initialize database schema
21
+ */
22
+ async initialize() {
23
+ if (this.initialized)
24
+ return;
25
+ const client = await this.pool.connect();
26
+ try {
27
+ await client.query(`
28
+ CREATE TABLE IF NOT EXISTS stories (
29
+ id VARCHAR(255) PRIMARY KEY,
30
+ title VARCHAR(500) NOT NULL,
31
+ description TEXT,
32
+ content TEXT NOT NULL,
33
+ prompt TEXT,
34
+ components TEXT[],
35
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
36
+ last_accessed TIMESTAMP WITH TIME ZONE DEFAULT NOW()
37
+ );
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_stories_created_at ON stories(created_at DESC);
40
+ CREATE INDEX IF NOT EXISTS idx_stories_last_accessed ON stories(last_accessed DESC);
41
+ `);
42
+ this.initialized = true;
43
+ console.log('✅ PostgreSQL story service initialized');
44
+ }
45
+ finally {
46
+ client.release();
47
+ }
48
+ }
49
+ /**
50
+ * Store a generated story
51
+ */
52
+ async storeStory(story) {
53
+ await this.initialize();
54
+ const client = await this.pool.connect();
55
+ try {
56
+ await client.query(`INSERT INTO stories (id, title, description, content, prompt, components, created_at, last_accessed)
57
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
58
+ ON CONFLICT (id) DO UPDATE SET
59
+ title = EXCLUDED.title,
60
+ description = EXCLUDED.description,
61
+ content = EXCLUDED.content,
62
+ prompt = EXCLUDED.prompt,
63
+ components = EXCLUDED.components,
64
+ last_accessed = EXCLUDED.last_accessed`, [
65
+ story.id,
66
+ story.title,
67
+ story.description,
68
+ story.content,
69
+ story.prompt || null,
70
+ story.components || [],
71
+ story.createdAt || new Date(),
72
+ new Date()
73
+ ]);
74
+ console.log(`✅ Stored story in PostgreSQL: ${story.id}`);
75
+ }
76
+ finally {
77
+ client.release();
78
+ }
79
+ }
80
+ /**
81
+ * Retrieve a story by ID
82
+ */
83
+ async getStory(id) {
84
+ await this.initialize();
85
+ const client = await this.pool.connect();
86
+ try {
87
+ // Update last_accessed and return the story
88
+ const result = await client.query(`UPDATE stories SET last_accessed = NOW()
89
+ WHERE id = $1
90
+ RETURNING id, title, description, content, prompt, components, created_at, last_accessed`, [id]);
91
+ if (result.rows.length === 0) {
92
+ return null;
93
+ }
94
+ const row = result.rows[0];
95
+ return {
96
+ id: row.id,
97
+ title: row.title,
98
+ description: row.description,
99
+ content: row.content,
100
+ prompt: row.prompt,
101
+ components: row.components || [],
102
+ createdAt: new Date(row.created_at),
103
+ lastAccessed: new Date(row.last_accessed)
104
+ };
105
+ }
106
+ finally {
107
+ client.release();
108
+ }
109
+ }
110
+ /**
111
+ * Get all stored stories
112
+ */
113
+ async getAllStories() {
114
+ await this.initialize();
115
+ const client = await this.pool.connect();
116
+ try {
117
+ const result = await client.query(`SELECT id, title, description, content, prompt, components, created_at, last_accessed
118
+ FROM stories
119
+ ORDER BY created_at DESC`);
120
+ return result.rows.map(row => ({
121
+ id: row.id,
122
+ title: row.title,
123
+ description: row.description,
124
+ content: row.content,
125
+ prompt: row.prompt,
126
+ components: row.components || [],
127
+ createdAt: new Date(row.created_at),
128
+ lastAccessed: new Date(row.last_accessed)
129
+ }));
130
+ }
131
+ finally {
132
+ client.release();
133
+ }
134
+ }
135
+ /**
136
+ * Delete a story by ID
137
+ */
138
+ async deleteStory(id) {
139
+ await this.initialize();
140
+ const client = await this.pool.connect();
141
+ try {
142
+ const result = await client.query(`DELETE FROM stories WHERE id = $1 RETURNING id`, [id]);
143
+ const deleted = result.rowCount !== null && result.rowCount > 0;
144
+ if (deleted) {
145
+ console.log(`✅ Deleted story from PostgreSQL: ${id}`);
146
+ }
147
+ return deleted;
148
+ }
149
+ finally {
150
+ client.release();
151
+ }
152
+ }
153
+ /**
154
+ * Clear all stories
155
+ */
156
+ async clearAllStories() {
157
+ await this.initialize();
158
+ const client = await this.pool.connect();
159
+ try {
160
+ await client.query('TRUNCATE TABLE stories');
161
+ console.log('✅ Cleared all stories from PostgreSQL');
162
+ }
163
+ finally {
164
+ client.release();
165
+ }
166
+ }
167
+ /**
168
+ * Get story content for Storybook integration
169
+ */
170
+ async getStoryContent(id) {
171
+ const story = await this.getStory(id);
172
+ return story ? story.content : null;
173
+ }
174
+ /**
175
+ * Get story metadata for listing
176
+ */
177
+ async getStoryMetadata() {
178
+ await this.initialize();
179
+ const client = await this.pool.connect();
180
+ try {
181
+ const result = await client.query(`SELECT id, title, description, content, created_at, last_accessed
182
+ FROM stories
183
+ ORDER BY created_at DESC`);
184
+ return result.rows.map(row => ({
185
+ id: row.id,
186
+ title: row.title,
187
+ description: row.description,
188
+ createdAt: new Date(row.created_at),
189
+ lastAccessed: new Date(row.last_accessed),
190
+ componentCount: this.countComponents(row.content)
191
+ }));
192
+ }
193
+ finally {
194
+ client.release();
195
+ }
196
+ }
197
+ /**
198
+ * Get storage usage statistics
199
+ */
200
+ async getStorageStats() {
201
+ await this.initialize();
202
+ const client = await this.pool.connect();
203
+ try {
204
+ const result = await client.query(`
205
+ SELECT
206
+ COUNT(*)::int as story_count,
207
+ COALESCE(SUM(LENGTH(content)), 0)::bigint as total_size,
208
+ COALESCE(AVG(LENGTH(content)), 0)::int as avg_size,
209
+ MIN(created_at) as oldest,
210
+ MAX(created_at) as newest
211
+ FROM stories
212
+ `);
213
+ const row = result.rows[0];
214
+ return {
215
+ storyCount: row.story_count,
216
+ totalSizeBytes: parseInt(row.total_size) || 0,
217
+ averageSizeBytes: row.avg_size || 0,
218
+ oldestStory: row.oldest ? new Date(row.oldest) : null,
219
+ newestStory: row.newest ? new Date(row.newest) : null
220
+ };
221
+ }
222
+ finally {
223
+ client.release();
224
+ }
225
+ }
226
+ /**
227
+ * Count unique components in story content
228
+ */
229
+ countComponents(content) {
230
+ const componentMatches = content.match(/<[A-Z][A-Za-z0-9]*\s/g);
231
+ return componentMatches ? new Set(componentMatches).size : 0;
232
+ }
233
+ /**
234
+ * Close the connection pool
235
+ */
236
+ async close() {
237
+ await this.pool.end();
238
+ console.log('PostgreSQL connection pool closed');
239
+ }
240
+ }
@@ -0,0 +1,22 @@
1
+ import type { IStoryService } from './storyServiceInterface.js';
2
+ import { StoryUIConfig } from '../story-ui.config.js';
3
+ /**
4
+ * Get or create the story service based on environment configuration
5
+ *
6
+ * - If DATABASE_URL is set, uses PostgreSQL for persistent storage
7
+ * - Otherwise, falls back to in-memory storage
8
+ */
9
+ export declare function getStoryService(config: StoryUIConfig): Promise<IStoryService>;
10
+ /**
11
+ * Check if PostgreSQL storage is configured
12
+ */
13
+ export declare function isPostgresConfigured(): boolean;
14
+ /**
15
+ * Get storage type description
16
+ */
17
+ export declare function getStorageType(): 'postgresql' | 'memory';
18
+ /**
19
+ * Reset the global service (for testing)
20
+ */
21
+ export declare function resetStoryService(): Promise<void>;
22
+ //# sourceMappingURL=storyServiceFactory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storyServiceFactory.d.ts","sourceRoot":"","sources":["../../story-generator/storyServiceFactory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA+C,MAAM,4BAA4B,CAAC;AAG7G,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAkEtD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBnF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,YAAY,GAAG,QAAQ,CAExD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvD"}
@@ -0,0 +1,97 @@
1
+ import { getInMemoryStoryService } from './inMemoryStoryService.js';
2
+ import { PostgresStoryService } from './postgresStoryService.js';
3
+ /**
4
+ * Async wrapper for InMemoryStoryService
5
+ * Makes the synchronous in-memory service conform to the async IStoryService interface
6
+ */
7
+ class AsyncInMemoryStoryService {
8
+ constructor(config) {
9
+ this.innerService = getInMemoryStoryService(config);
10
+ }
11
+ async initialize() {
12
+ // No-op for in-memory
13
+ }
14
+ async storeStory(story) {
15
+ this.innerService.storeStory(story);
16
+ }
17
+ async getStory(id) {
18
+ return this.innerService.getStory(id);
19
+ }
20
+ async getAllStories() {
21
+ return this.innerService.getAllStories();
22
+ }
23
+ async deleteStory(id) {
24
+ return this.innerService.deleteStory(id);
25
+ }
26
+ async clearAllStories() {
27
+ this.innerService.clearAllStories();
28
+ }
29
+ async getStoryContent(id) {
30
+ return this.innerService.getStoryContent(id);
31
+ }
32
+ async getStoryMetadata() {
33
+ return this.innerService.getStoryMetadata();
34
+ }
35
+ async getStorageStats() {
36
+ const stats = this.innerService.getMemoryStats();
37
+ return {
38
+ storyCount: stats.storyCount,
39
+ totalSizeBytes: stats.totalSizeBytes,
40
+ averageSizeBytes: stats.averageSizeBytes,
41
+ oldestStory: stats.oldestStory,
42
+ newestStory: stats.newestStory
43
+ };
44
+ }
45
+ async close() {
46
+ // No-op for in-memory
47
+ }
48
+ }
49
+ /**
50
+ * Global story service instance
51
+ */
52
+ let globalStoryService = null;
53
+ /**
54
+ * Get or create the story service based on environment configuration
55
+ *
56
+ * - If DATABASE_URL is set, uses PostgreSQL for persistent storage
57
+ * - Otherwise, falls back to in-memory storage
58
+ */
59
+ export async function getStoryService(config) {
60
+ if (globalStoryService) {
61
+ return globalStoryService;
62
+ }
63
+ const databaseUrl = process.env.DATABASE_URL;
64
+ if (databaseUrl) {
65
+ console.log('🗄️ Using PostgreSQL for story persistence');
66
+ const pgService = new PostgresStoryService(databaseUrl);
67
+ await pgService.initialize();
68
+ globalStoryService = pgService;
69
+ }
70
+ else {
71
+ console.log('💾 Using in-memory storage (no DATABASE_URL configured)');
72
+ globalStoryService = new AsyncInMemoryStoryService(config);
73
+ await globalStoryService.initialize();
74
+ }
75
+ return globalStoryService;
76
+ }
77
+ /**
78
+ * Check if PostgreSQL storage is configured
79
+ */
80
+ export function isPostgresConfigured() {
81
+ return !!process.env.DATABASE_URL;
82
+ }
83
+ /**
84
+ * Get storage type description
85
+ */
86
+ export function getStorageType() {
87
+ return process.env.DATABASE_URL ? 'postgresql' : 'memory';
88
+ }
89
+ /**
90
+ * Reset the global service (for testing)
91
+ */
92
+ export async function resetStoryService() {
93
+ if (globalStoryService) {
94
+ await globalStoryService.close();
95
+ globalStoryService = null;
96
+ }
97
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Story Service Interface
3
+ * Common interface for both in-memory and PostgreSQL story storage
4
+ */
5
+ /**
6
+ * Generated story interface
7
+ */
8
+ export interface GeneratedStory {
9
+ id: string;
10
+ title: string;
11
+ description: string;
12
+ content: string;
13
+ createdAt: Date;
14
+ lastAccessed: Date;
15
+ prompt?: string;
16
+ components?: string[];
17
+ }
18
+ /**
19
+ * Story metadata for listing
20
+ */
21
+ export interface StoryMetadata {
22
+ id: string;
23
+ title: string;
24
+ description: string;
25
+ createdAt: Date;
26
+ lastAccessed: Date;
27
+ componentCount: number;
28
+ }
29
+ /**
30
+ * Memory/storage usage statistics
31
+ */
32
+ export interface StorageStats {
33
+ storyCount: number;
34
+ totalSizeBytes: number;
35
+ averageSizeBytes: number;
36
+ oldestStory: Date | null;
37
+ newestStory: Date | null;
38
+ }
39
+ /**
40
+ * Story Service Interface
41
+ * All methods are async to support both in-memory and database backends
42
+ */
43
+ export interface IStoryService {
44
+ /**
45
+ * Initialize the service (create tables, etc.)
46
+ */
47
+ initialize(): Promise<void>;
48
+ /**
49
+ * Store a generated story
50
+ */
51
+ storeStory(story: GeneratedStory): Promise<void>;
52
+ /**
53
+ * Retrieve a story by ID
54
+ */
55
+ getStory(id: string): Promise<GeneratedStory | null>;
56
+ /**
57
+ * Get all stored stories
58
+ */
59
+ getAllStories(): Promise<GeneratedStory[]>;
60
+ /**
61
+ * Delete a story by ID
62
+ */
63
+ deleteStory(id: string): Promise<boolean>;
64
+ /**
65
+ * Clear all stories
66
+ */
67
+ clearAllStories(): Promise<void>;
68
+ /**
69
+ * Get story content for Storybook integration
70
+ */
71
+ getStoryContent(id: string): Promise<string | null>;
72
+ /**
73
+ * Get story metadata for listing
74
+ */
75
+ getStoryMetadata(): Promise<StoryMetadata[]>;
76
+ /**
77
+ * Get storage usage statistics
78
+ */
79
+ getStorageStats(): Promise<StorageStats>;
80
+ /**
81
+ * Close any connections (for cleanup)
82
+ */
83
+ close(): Promise<void>;
84
+ }
85
+ //# sourceMappingURL=storyServiceInterface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storyServiceInterface.d.ts","sourceRoot":"","sources":["../../story-generator/storyServiceInterface.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjD;;OAEG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAErD;;OAEG;IACH,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAE3C;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1C;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjC;;OAEG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEpD;;OAEG;IACH,gBAAgB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAE7C;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzC;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Story Service Interface
3
+ * Common interface for both in-memory and PostgreSQL story storage
4
+ */
5
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,9 +36,7 @@
36
36
  "release:dry-run": "semantic-release --dry-run",
37
37
  "commit": "cz",
38
38
  "prepare": "husky",
39
- "story-ui": "story-ui start --port 4001",
40
- "deploy:edge": "./scripts/deploy-edge.sh",
41
- "deploy:edge:dry-run": "./scripts/deploy-edge.sh --dry-run"
39
+ "story-ui": "story-ui start --port 4001"
42
40
  },
43
41
  "keywords": [
44
42
  "storybook",
@@ -78,6 +76,7 @@
78
76
  "@emotion/styled": "^11.14.1",
79
77
  "@modelcontextprotocol/sdk": "^0.5.0",
80
78
  "@mui/material": "^7.2.0",
79
+ "@types/pg": "^8.15.6",
81
80
  "chalk": "^5.3.0",
82
81
  "commander": "^11.0.0",
83
82
  "cors": "^2.8.5",
@@ -86,6 +85,7 @@
86
85
  "glob": "^11.0.3",
87
86
  "inquirer": "^9.2.0",
88
87
  "node-fetch": "^2.6.7",
88
+ "pg": "^8.16.3",
89
89
  "typescript": "^5.8.3",
90
90
  "zod": "^3.22.4"
91
91
  },
@@ -105,11 +105,11 @@
105
105
  "@types/node": "^20.4.2",
106
106
  "@types/node-fetch": "^2.6.12",
107
107
  "commitizen": "^4.3.1",
108
+ "concurrently": "^8.2.0",
108
109
  "cz-conventional-changelog": "^3.3.0",
109
110
  "husky": "^9.1.7",
110
111
  "semantic-release": "^24.2.0",
111
- "ts-node": "^10.9.2",
112
- "concurrently": "^8.2.0"
112
+ "ts-node": "^10.9.2"
113
113
  },
114
114
  "peerDependencies": {
115
115
  "@storybook/react": ">=6.0.0",