@nextsparkjs/core 0.1.0-beta.65 → 0.1.0-beta.67
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/entities/patterns/messages/de.json +76 -0
- package/dist/entities/patterns/messages/en.json +87 -0
- package/dist/entities/patterns/messages/es.json +86 -0
- package/dist/entities/patterns/messages/fr.json +76 -0
- package/dist/entities/patterns/messages/it.json +76 -0
- package/dist/entities/patterns/messages/pt.json +76 -0
- package/dist/styles/classes.json +1 -1
- package/package.json +6 -1
- package/src/entities/index.ts +24 -0
- package/src/entities/patterns/index.ts +27 -0
- package/src/entities/patterns/messages/de.json +76 -0
- package/src/entities/patterns/messages/en.json +87 -0
- package/src/entities/patterns/messages/es.json +86 -0
- package/src/entities/patterns/messages/fr.json +76 -0
- package/src/entities/patterns/messages/it.json +76 -0
- package/src/entities/patterns/messages/pt.json +76 -0
- package/src/entities/patterns/patterns.config.ts +84 -0
- package/src/entities/patterns/patterns.fields.ts +104 -0
- package/src/entities/patterns/patterns.service.ts +718 -0
- package/src/entities/patterns/patterns.types.ts +118 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patterns Service
|
|
3
|
+
*
|
|
4
|
+
* Provides data access methods for patterns.
|
|
5
|
+
* Patterns is a team-scoped entity (shared: true) - all team members see the same patterns.
|
|
6
|
+
*
|
|
7
|
+
* All methods require authentication and use RLS with team isolation.
|
|
8
|
+
*
|
|
9
|
+
* @module PatternsService
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '../../lib/db'
|
|
13
|
+
import type {
|
|
14
|
+
Pattern,
|
|
15
|
+
CreatePatternInput,
|
|
16
|
+
UpdatePatternInput,
|
|
17
|
+
PatternListOptions,
|
|
18
|
+
PatternListResult,
|
|
19
|
+
PatternStatus
|
|
20
|
+
} from './patterns.types'
|
|
21
|
+
import { isPatternReference } from './patterns.types'
|
|
22
|
+
|
|
23
|
+
// Database row type for pattern
|
|
24
|
+
interface DbPattern {
|
|
25
|
+
id: string
|
|
26
|
+
userId: string
|
|
27
|
+
teamId: string
|
|
28
|
+
title: string
|
|
29
|
+
slug: string
|
|
30
|
+
blocks: unknown // JSONB
|
|
31
|
+
status: PatternStatus
|
|
32
|
+
description: string | null
|
|
33
|
+
createdAt: string
|
|
34
|
+
updatedAt: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helper function to check if blocks contain pattern references
|
|
39
|
+
* Used to prevent nesting patterns inside patterns
|
|
40
|
+
*/
|
|
41
|
+
function containsPatternReference(blocks: unknown[]): boolean {
|
|
42
|
+
if (!Array.isArray(blocks)) return false
|
|
43
|
+
return blocks.some(block => isPatternReference(block))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class PatternsService {
|
|
47
|
+
// ============================================
|
|
48
|
+
// READ OPERATIONS
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get a pattern by ID
|
|
53
|
+
*
|
|
54
|
+
* Respects RLS policies. Since patterns has shared: true,
|
|
55
|
+
* all team members can access patterns from their team.
|
|
56
|
+
*
|
|
57
|
+
* @param id - Pattern ID
|
|
58
|
+
* @param userId - Current user ID for RLS
|
|
59
|
+
* @returns Pattern data or null if not found/not authorized
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const pattern = await PatternsService.getById('pattern-uuid', currentUserId)
|
|
63
|
+
*/
|
|
64
|
+
static async getById(
|
|
65
|
+
id: string,
|
|
66
|
+
userId: string
|
|
67
|
+
): Promise<Pattern | null> {
|
|
68
|
+
try {
|
|
69
|
+
if (!id?.trim()) {
|
|
70
|
+
throw new Error('Pattern ID is required')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!userId?.trim()) {
|
|
74
|
+
throw new Error('User ID is required for authentication')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pattern = await queryOneWithRLS<DbPattern>(
|
|
78
|
+
`
|
|
79
|
+
SELECT
|
|
80
|
+
id,
|
|
81
|
+
"userId",
|
|
82
|
+
"teamId",
|
|
83
|
+
title,
|
|
84
|
+
slug,
|
|
85
|
+
blocks,
|
|
86
|
+
status,
|
|
87
|
+
description,
|
|
88
|
+
"createdAt",
|
|
89
|
+
"updatedAt"
|
|
90
|
+
FROM "patterns"
|
|
91
|
+
WHERE id = $1
|
|
92
|
+
`,
|
|
93
|
+
[id],
|
|
94
|
+
userId
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if (!pattern) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
id: pattern.id,
|
|
103
|
+
userId: pattern.userId,
|
|
104
|
+
teamId: pattern.teamId,
|
|
105
|
+
title: pattern.title,
|
|
106
|
+
slug: pattern.slug,
|
|
107
|
+
blocks: Array.isArray(pattern.blocks) ? pattern.blocks : [],
|
|
108
|
+
status: pattern.status,
|
|
109
|
+
description: pattern.description ?? undefined,
|
|
110
|
+
createdAt: pattern.createdAt,
|
|
111
|
+
updatedAt: pattern.updatedAt
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[PatternsService.getById] Error:', error)
|
|
115
|
+
throw new Error(
|
|
116
|
+
error instanceof Error ? error.message : 'Failed to fetch pattern'
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a pattern by slug and team ID
|
|
123
|
+
*
|
|
124
|
+
* @param slug - Pattern slug
|
|
125
|
+
* @param teamId - Team ID
|
|
126
|
+
* @param userId - Current user ID for RLS
|
|
127
|
+
* @returns Pattern data or null if not found
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* const pattern = await PatternsService.getBySlug('newsletter-cta', teamId, userId)
|
|
131
|
+
*/
|
|
132
|
+
static async getBySlug(
|
|
133
|
+
slug: string,
|
|
134
|
+
teamId: string,
|
|
135
|
+
userId: string
|
|
136
|
+
): Promise<Pattern | null> {
|
|
137
|
+
try {
|
|
138
|
+
if (!slug?.trim()) {
|
|
139
|
+
throw new Error('Slug is required')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!teamId?.trim()) {
|
|
143
|
+
throw new Error('Team ID is required')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!userId?.trim()) {
|
|
147
|
+
throw new Error('User ID is required for authentication')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pattern = await queryOneWithRLS<DbPattern>(
|
|
151
|
+
`
|
|
152
|
+
SELECT
|
|
153
|
+
id,
|
|
154
|
+
"userId",
|
|
155
|
+
"teamId",
|
|
156
|
+
title,
|
|
157
|
+
slug,
|
|
158
|
+
blocks,
|
|
159
|
+
status,
|
|
160
|
+
description,
|
|
161
|
+
"createdAt",
|
|
162
|
+
"updatedAt"
|
|
163
|
+
FROM "patterns"
|
|
164
|
+
WHERE slug = $1 AND "teamId" = $2
|
|
165
|
+
`,
|
|
166
|
+
[slug, teamId],
|
|
167
|
+
userId
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (!pattern) {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: pattern.id,
|
|
176
|
+
userId: pattern.userId,
|
|
177
|
+
teamId: pattern.teamId,
|
|
178
|
+
title: pattern.title,
|
|
179
|
+
slug: pattern.slug,
|
|
180
|
+
blocks: Array.isArray(pattern.blocks) ? pattern.blocks : [],
|
|
181
|
+
status: pattern.status,
|
|
182
|
+
description: pattern.description ?? undefined,
|
|
183
|
+
createdAt: pattern.createdAt,
|
|
184
|
+
updatedAt: pattern.updatedAt
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('[PatternsService.getBySlug] Error:', error)
|
|
188
|
+
throw new Error(
|
|
189
|
+
error instanceof Error ? error.message : 'Failed to fetch pattern by slug'
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* List patterns with pagination and filtering
|
|
196
|
+
*
|
|
197
|
+
* @param userId - Current user ID for RLS
|
|
198
|
+
* @param teamId - Team ID
|
|
199
|
+
* @param options - List options (limit, offset, status, orderBy, orderDir)
|
|
200
|
+
* @returns Object with patterns array and total count
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* const { data, total } = await PatternsService.list(userId, teamId, {
|
|
204
|
+
* status: 'published',
|
|
205
|
+
* limit: 10
|
|
206
|
+
* })
|
|
207
|
+
*/
|
|
208
|
+
static async list(
|
|
209
|
+
userId: string,
|
|
210
|
+
teamId: string,
|
|
211
|
+
options: PatternListOptions = {}
|
|
212
|
+
): Promise<PatternListResult> {
|
|
213
|
+
try {
|
|
214
|
+
if (!userId?.trim()) {
|
|
215
|
+
throw new Error('User ID is required for authentication')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!teamId?.trim()) {
|
|
219
|
+
throw new Error('Team ID is required')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const {
|
|
223
|
+
limit = 20,
|
|
224
|
+
offset = 0,
|
|
225
|
+
status,
|
|
226
|
+
orderBy = 'createdAt',
|
|
227
|
+
orderDir = 'desc'
|
|
228
|
+
} = options
|
|
229
|
+
|
|
230
|
+
// Build WHERE clause
|
|
231
|
+
const conditions: string[] = ['"teamId" = $1']
|
|
232
|
+
const params: unknown[] = [teamId]
|
|
233
|
+
let paramIndex = 2
|
|
234
|
+
|
|
235
|
+
if (status) {
|
|
236
|
+
conditions.push(`status = $${paramIndex}`)
|
|
237
|
+
params.push(status)
|
|
238
|
+
paramIndex++
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`
|
|
242
|
+
|
|
243
|
+
// Validate orderBy to prevent SQL injection
|
|
244
|
+
const validOrderBy = ['title', 'slug', 'status', 'createdAt', 'updatedAt'].includes(orderBy)
|
|
245
|
+
? orderBy
|
|
246
|
+
: 'createdAt'
|
|
247
|
+
const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
|
|
248
|
+
|
|
249
|
+
// Map field names to database columns
|
|
250
|
+
const orderColumnMap: Record<string, string> = {
|
|
251
|
+
title: 'title',
|
|
252
|
+
slug: 'slug',
|
|
253
|
+
status: 'status',
|
|
254
|
+
createdAt: '"createdAt"',
|
|
255
|
+
updatedAt: '"updatedAt"'
|
|
256
|
+
}
|
|
257
|
+
const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
|
|
258
|
+
|
|
259
|
+
// Get total count
|
|
260
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
261
|
+
`SELECT COUNT(*)::text as count FROM "patterns" ${whereClause}`,
|
|
262
|
+
params,
|
|
263
|
+
userId
|
|
264
|
+
)
|
|
265
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
266
|
+
|
|
267
|
+
// Get patterns
|
|
268
|
+
params.push(limit, offset)
|
|
269
|
+
const patterns = await queryWithRLS<DbPattern>(
|
|
270
|
+
`
|
|
271
|
+
SELECT
|
|
272
|
+
id,
|
|
273
|
+
"userId",
|
|
274
|
+
"teamId",
|
|
275
|
+
title,
|
|
276
|
+
slug,
|
|
277
|
+
blocks,
|
|
278
|
+
status,
|
|
279
|
+
description,
|
|
280
|
+
"createdAt",
|
|
281
|
+
"updatedAt"
|
|
282
|
+
FROM "patterns"
|
|
283
|
+
${whereClause}
|
|
284
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
285
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
286
|
+
`,
|
|
287
|
+
params,
|
|
288
|
+
userId
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
data: patterns.map((pattern) => ({
|
|
293
|
+
id: pattern.id,
|
|
294
|
+
userId: pattern.userId,
|
|
295
|
+
teamId: pattern.teamId,
|
|
296
|
+
title: pattern.title,
|
|
297
|
+
slug: pattern.slug,
|
|
298
|
+
blocks: Array.isArray(pattern.blocks) ? pattern.blocks : [],
|
|
299
|
+
status: pattern.status,
|
|
300
|
+
description: pattern.description ?? undefined,
|
|
301
|
+
createdAt: pattern.createdAt,
|
|
302
|
+
updatedAt: pattern.updatedAt
|
|
303
|
+
})),
|
|
304
|
+
total
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('[PatternsService.list] Error:', error)
|
|
308
|
+
throw new Error(
|
|
309
|
+
error instanceof Error ? error.message : 'Failed to list patterns'
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* List only published patterns for a team
|
|
316
|
+
*
|
|
317
|
+
* Convenience method to fetch all published patterns.
|
|
318
|
+
* Used by the block picker to show available patterns for insertion.
|
|
319
|
+
*
|
|
320
|
+
* @param userId - Current user ID for RLS
|
|
321
|
+
* @param teamId - Team ID
|
|
322
|
+
* @returns Array of published patterns
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* const publishedPatterns = await PatternsService.listPublished(userId, teamId)
|
|
326
|
+
*/
|
|
327
|
+
static async listPublished(
|
|
328
|
+
userId: string,
|
|
329
|
+
teamId: string
|
|
330
|
+
): Promise<Pattern[]> {
|
|
331
|
+
try {
|
|
332
|
+
const { data } = await this.list(userId, teamId, {
|
|
333
|
+
status: 'published',
|
|
334
|
+
limit: 1000, // Large limit to get all published patterns
|
|
335
|
+
orderBy: 'title',
|
|
336
|
+
orderDir: 'asc'
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
return data
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('[PatternsService.listPublished] Error:', error)
|
|
342
|
+
throw new Error(
|
|
343
|
+
error instanceof Error ? error.message : 'Failed to fetch published patterns'
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get multiple patterns by IDs (batch fetch)
|
|
350
|
+
*
|
|
351
|
+
* Used for resolving pattern references in pages.
|
|
352
|
+
* Efficiently fetches all referenced patterns in a single query.
|
|
353
|
+
*
|
|
354
|
+
* @param ids - Array of pattern IDs
|
|
355
|
+
* @param userId - Current user ID for RLS
|
|
356
|
+
* @returns Array of patterns (may be fewer than ids if some not found)
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* const patternRefs = [{ ref: 'uuid-1' }, { ref: 'uuid-2' }]
|
|
360
|
+
* const patterns = await PatternsService.getByIds(
|
|
361
|
+
* patternRefs.map(p => p.ref),
|
|
362
|
+
* userId
|
|
363
|
+
* )
|
|
364
|
+
*/
|
|
365
|
+
static async getByIds(
|
|
366
|
+
ids: string[],
|
|
367
|
+
userId: string
|
|
368
|
+
): Promise<Pattern[]> {
|
|
369
|
+
try {
|
|
370
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
371
|
+
return []
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!userId?.trim()) {
|
|
375
|
+
throw new Error('User ID is required for authentication')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Filter out empty IDs
|
|
379
|
+
const validIds = ids.filter(id => id && id.trim())
|
|
380
|
+
if (validIds.length === 0) {
|
|
381
|
+
return []
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Build WHERE IN clause
|
|
385
|
+
const placeholders = validIds.map((_, i) => `$${i + 1}`).join(', ')
|
|
386
|
+
|
|
387
|
+
const patterns = await queryWithRLS<DbPattern>(
|
|
388
|
+
`
|
|
389
|
+
SELECT
|
|
390
|
+
id,
|
|
391
|
+
"userId",
|
|
392
|
+
"teamId",
|
|
393
|
+
title,
|
|
394
|
+
slug,
|
|
395
|
+
blocks,
|
|
396
|
+
status,
|
|
397
|
+
description,
|
|
398
|
+
"createdAt",
|
|
399
|
+
"updatedAt"
|
|
400
|
+
FROM "patterns"
|
|
401
|
+
WHERE id IN (${placeholders})
|
|
402
|
+
`,
|
|
403
|
+
validIds,
|
|
404
|
+
userId
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return patterns.map((pattern) => ({
|
|
408
|
+
id: pattern.id,
|
|
409
|
+
userId: pattern.userId,
|
|
410
|
+
teamId: pattern.teamId,
|
|
411
|
+
title: pattern.title,
|
|
412
|
+
slug: pattern.slug,
|
|
413
|
+
blocks: Array.isArray(pattern.blocks) ? pattern.blocks : [],
|
|
414
|
+
status: pattern.status,
|
|
415
|
+
description: pattern.description ?? undefined,
|
|
416
|
+
createdAt: pattern.createdAt,
|
|
417
|
+
updatedAt: pattern.updatedAt
|
|
418
|
+
}))
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error('[PatternsService.getByIds] Error:', error)
|
|
421
|
+
throw new Error(
|
|
422
|
+
error instanceof Error ? error.message : 'Failed to fetch patterns by IDs'
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================
|
|
428
|
+
// WRITE OPERATIONS
|
|
429
|
+
// ============================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Create a new pattern
|
|
433
|
+
*
|
|
434
|
+
* @param userId - Current user ID (will be pattern owner)
|
|
435
|
+
* @param teamId - Team ID (pattern belongs to team)
|
|
436
|
+
* @param data - Pattern data
|
|
437
|
+
* @returns Created pattern
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* const pattern = await PatternsService.create(userId, teamId, {
|
|
441
|
+
* title: 'Newsletter CTA',
|
|
442
|
+
* slug: 'newsletter-cta',
|
|
443
|
+
* blocks: [...],
|
|
444
|
+
* status: 'draft'
|
|
445
|
+
* })
|
|
446
|
+
*/
|
|
447
|
+
static async create(
|
|
448
|
+
userId: string,
|
|
449
|
+
teamId: string,
|
|
450
|
+
data: CreatePatternInput
|
|
451
|
+
): Promise<Pattern> {
|
|
452
|
+
try {
|
|
453
|
+
if (!userId?.trim()) {
|
|
454
|
+
throw new Error('User ID is required')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!teamId?.trim()) {
|
|
458
|
+
throw new Error('Team ID is required')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!data.title?.trim()) {
|
|
462
|
+
throw new Error('Title is required')
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!data.slug?.trim()) {
|
|
466
|
+
throw new Error('Slug is required')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Validate: patterns cannot contain other patterns (prevent nesting)
|
|
470
|
+
if (data.blocks && containsPatternReference(data.blocks)) {
|
|
471
|
+
throw new Error('Patterns cannot contain other patterns')
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const result = await mutateWithRLS<DbPattern>(
|
|
475
|
+
`
|
|
476
|
+
INSERT INTO "patterns" (
|
|
477
|
+
"userId",
|
|
478
|
+
"teamId",
|
|
479
|
+
title,
|
|
480
|
+
slug,
|
|
481
|
+
blocks,
|
|
482
|
+
status,
|
|
483
|
+
description
|
|
484
|
+
)
|
|
485
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
486
|
+
RETURNING
|
|
487
|
+
id,
|
|
488
|
+
"userId",
|
|
489
|
+
"teamId",
|
|
490
|
+
title,
|
|
491
|
+
slug,
|
|
492
|
+
blocks,
|
|
493
|
+
status,
|
|
494
|
+
description,
|
|
495
|
+
"createdAt",
|
|
496
|
+
"updatedAt"
|
|
497
|
+
`,
|
|
498
|
+
[
|
|
499
|
+
userId,
|
|
500
|
+
teamId,
|
|
501
|
+
data.title,
|
|
502
|
+
data.slug,
|
|
503
|
+
JSON.stringify(data.blocks || []),
|
|
504
|
+
data.status || 'draft',
|
|
505
|
+
data.description || null
|
|
506
|
+
],
|
|
507
|
+
userId
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if (!result || result.rows.length === 0) {
|
|
511
|
+
throw new Error('Failed to create pattern')
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const pattern = result.rows[0]
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
id: pattern.id,
|
|
518
|
+
userId: pattern.userId,
|
|
519
|
+
teamId: pattern.teamId,
|
|
520
|
+
title: pattern.title,
|
|
521
|
+
slug: pattern.slug,
|
|
522
|
+
blocks: Array.isArray(pattern.blocks) ? pattern.blocks : [],
|
|
523
|
+
status: pattern.status,
|
|
524
|
+
description: pattern.description ?? undefined,
|
|
525
|
+
createdAt: pattern.createdAt,
|
|
526
|
+
updatedAt: pattern.updatedAt
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error('[PatternsService.create] Error:', error)
|
|
530
|
+
throw new Error(
|
|
531
|
+
error instanceof Error ? error.message : 'Failed to create pattern'
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Update an existing pattern
|
|
538
|
+
*
|
|
539
|
+
* @param id - Pattern ID
|
|
540
|
+
* @param userId - Current user ID for RLS
|
|
541
|
+
* @param data - Pattern data to update
|
|
542
|
+
* @returns Updated pattern
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* const pattern = await PatternsService.update('pattern-id', userId, {
|
|
546
|
+
* title: 'Updated Title',
|
|
547
|
+
* status: 'published'
|
|
548
|
+
* })
|
|
549
|
+
*/
|
|
550
|
+
static async update(
|
|
551
|
+
id: string,
|
|
552
|
+
userId: string,
|
|
553
|
+
data: UpdatePatternInput
|
|
554
|
+
): Promise<Pattern> {
|
|
555
|
+
try {
|
|
556
|
+
if (!id?.trim()) {
|
|
557
|
+
throw new Error('Pattern ID is required')
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!userId?.trim()) {
|
|
561
|
+
throw new Error('User ID is required')
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Validate: patterns cannot contain other patterns (prevent nesting)
|
|
565
|
+
if (data.blocks !== undefined && containsPatternReference(data.blocks)) {
|
|
566
|
+
throw new Error('Patterns cannot contain other patterns')
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Build SET clause dynamically
|
|
570
|
+
const updates: string[] = []
|
|
571
|
+
const params: unknown[] = []
|
|
572
|
+
let paramIndex = 1
|
|
573
|
+
|
|
574
|
+
if (data.title !== undefined) {
|
|
575
|
+
updates.push(`title = $${paramIndex}`)
|
|
576
|
+
params.push(data.title)
|
|
577
|
+
paramIndex++
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (data.slug !== undefined) {
|
|
581
|
+
updates.push(`slug = $${paramIndex}`)
|
|
582
|
+
params.push(data.slug)
|
|
583
|
+
paramIndex++
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (data.blocks !== undefined) {
|
|
587
|
+
updates.push(`blocks = $${paramIndex}`)
|
|
588
|
+
params.push(JSON.stringify(data.blocks))
|
|
589
|
+
paramIndex++
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (data.status !== undefined) {
|
|
593
|
+
updates.push(`status = $${paramIndex}`)
|
|
594
|
+
params.push(data.status)
|
|
595
|
+
paramIndex++
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (data.description !== undefined) {
|
|
599
|
+
updates.push(`description = $${paramIndex}`)
|
|
600
|
+
params.push(data.description || null)
|
|
601
|
+
paramIndex++
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (updates.length === 0) {
|
|
605
|
+
throw new Error('No fields to update')
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
params.push(id)
|
|
609
|
+
|
|
610
|
+
const result = await mutateWithRLS<DbPattern>(
|
|
611
|
+
`
|
|
612
|
+
UPDATE "patterns"
|
|
613
|
+
SET ${updates.join(', ')}
|
|
614
|
+
WHERE id = $${paramIndex}
|
|
615
|
+
RETURNING
|
|
616
|
+
id,
|
|
617
|
+
"userId",
|
|
618
|
+
"teamId",
|
|
619
|
+
title,
|
|
620
|
+
slug,
|
|
621
|
+
blocks,
|
|
622
|
+
status,
|
|
623
|
+
description,
|
|
624
|
+
"createdAt",
|
|
625
|
+
"updatedAt"
|
|
626
|
+
`,
|
|
627
|
+
params,
|
|
628
|
+
userId
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
if (!result || result.rows.length === 0) {
|
|
632
|
+
throw new Error('Pattern not found or not authorized')
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const pattern = result.rows[0]
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
id: pattern.id,
|
|
639
|
+
userId: pattern.userId,
|
|
640
|
+
teamId: pattern.teamId,
|
|
641
|
+
title: pattern.title,
|
|
642
|
+
slug: pattern.slug,
|
|
643
|
+
blocks: Array.isArray(pattern.blocks) ? pattern.blocks : [],
|
|
644
|
+
status: pattern.status,
|
|
645
|
+
description: pattern.description ?? undefined,
|
|
646
|
+
createdAt: pattern.createdAt,
|
|
647
|
+
updatedAt: pattern.updatedAt
|
|
648
|
+
}
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.error('[PatternsService.update] Error:', error)
|
|
651
|
+
throw new Error(
|
|
652
|
+
error instanceof Error ? error.message : 'Failed to update pattern'
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Delete a pattern
|
|
659
|
+
*
|
|
660
|
+
* Uses "Lazy Cleanup" strategy: patterns can be deleted even if in use.
|
|
661
|
+
* - ON DELETE CASCADE cleans up pattern_usages
|
|
662
|
+
* - Orphaned PatternReferences in entities are filtered out on next save
|
|
663
|
+
* - Public rendering gracefully skips missing patterns
|
|
664
|
+
*
|
|
665
|
+
* @param id - Pattern ID
|
|
666
|
+
* @param userId - Current user ID for RLS
|
|
667
|
+
* @returns True if deleted successfully
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* await PatternsService.delete('pattern-id', userId)
|
|
671
|
+
*/
|
|
672
|
+
static async delete(
|
|
673
|
+
id: string,
|
|
674
|
+
userId: string
|
|
675
|
+
): Promise<boolean> {
|
|
676
|
+
try {
|
|
677
|
+
if (!id?.trim()) {
|
|
678
|
+
throw new Error('Pattern ID is required')
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!userId?.trim()) {
|
|
682
|
+
throw new Error('User ID is required')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Informative: Log usage count for auditing
|
|
686
|
+
// Uses dynamic import to avoid circular dependency
|
|
687
|
+
try {
|
|
688
|
+
const { PatternUsageService } = await import('../../lib/services/pattern-usage.service')
|
|
689
|
+
const usageCount = await PatternUsageService.getUsageCount(id, userId)
|
|
690
|
+
if (usageCount > 0) {
|
|
691
|
+
console.info(
|
|
692
|
+
`[PatternsService.delete] Deleting pattern ${id} with ${usageCount} active usages. ` +
|
|
693
|
+
`References will be cleaned up on next entity save.`
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
// Don't fail delete if usage count check fails
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const result = await mutateWithRLS<DbPattern>(
|
|
701
|
+
`
|
|
702
|
+
DELETE FROM "patterns"
|
|
703
|
+
WHERE id = $1
|
|
704
|
+
RETURNING id
|
|
705
|
+
`,
|
|
706
|
+
[id],
|
|
707
|
+
userId
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
return result && result.rows.length > 0
|
|
711
|
+
} catch (error) {
|
|
712
|
+
console.error('[PatternsService.delete] Error:', error)
|
|
713
|
+
throw new Error(
|
|
714
|
+
error instanceof Error ? error.message : 'Failed to delete pattern'
|
|
715
|
+
)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|