nca-ai-cms-astro-plugin 1.1.1 → 1.1.3

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,613 @@
1
+ # JSON Database Import/Export Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Replace the SQLite file-swap upload (which breaks the live DB connection and causes infinite redirect loops) with JSON-based export/import that uses the live `astro:db` connection for normal delete-and-insert operations.
6
+
7
+ **Architecture:** Two new API endpoints (`/api/db/export` GET, `/api/db/import` POST) that serialize/deserialize the three content tables (SiteSettings, Prompts, ScheduledPosts) as JSON. Import uses replace semantics: delete all existing rows in each table, then insert the imported rows — matching the old file-swap behavior but through the live `astro:db` connection. No file replacement, no connection disruption, no process restart. Sessions are excluded (they're per-instance auth state, so the user stays logged in).
8
+
9
+ **Tech Stack:** Astro API routes, `astro:db` (existing), `db.batch()` for atomic operations, `node:test` for tests, no new dependencies.
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | Action | File | Responsibility |
16
+ |--------|------|----------------|
17
+ | Create | `src/services/DbTransferService.ts` | Export all content tables to JSON, import JSON back via delete+insert |
18
+ | Create | `src/services/DbTransferService.test.ts` | Unit tests for validation logic (pure functions, no DB dependency) |
19
+ | Create | `src/api/db/export.ts` | GET endpoint — returns JSON dump |
20
+ | Create | `src/api/db/import.ts` | POST endpoint — accepts JSON, replaces data via DbTransferService |
21
+
22
+ **Tables included in export/import:**
23
+ - `SiteSettings` (key, value, updatedAt)
24
+ - `Prompts` (id, name, category, promptText, updatedAt)
25
+ - `ScheduledPosts` (id, input, inputType, scheduledDate, status, generatedTitle, generatedDescription, generatedContent, generatedTags, generatedImageData, generatedImageAlt, publishedPath, createdAt)
26
+
27
+ **Tables excluded:**
28
+ - `Sessions` — per-instance auth tokens, must not be transferred between environments
29
+
30
+ ---
31
+
32
+ ### Task 1: DbTransferService — Validation Logic and Tests
33
+
34
+ **Files:**
35
+ - Create: `src/services/DbTransferService.ts`
36
+ - Create: `src/services/DbTransferService.test.ts`
37
+
38
+ - [ ] **Step 1: Write failing tests for validation**
39
+
40
+ In `src/services/DbTransferService.test.ts`:
41
+
42
+ ```typescript
43
+ import { describe, it } from 'node:test';
44
+ import assert from 'node:assert/strict';
45
+ import { validateImportPayload, type DbTransferPayload } from './DbTransferService.js';
46
+
47
+ describe('validateImportPayload', () => {
48
+ it('accepts a valid payload with all three tables', () => {
49
+ const payload: DbTransferPayload = {
50
+ version: 1,
51
+ exportedAt: '2026-03-24T12:00:00.000Z',
52
+ tables: {
53
+ siteSettings: [{ key: 'content.branche', value: 'Tech', updatedAt: '2026-03-24T12:00:00.000Z' }],
54
+ prompts: [],
55
+ scheduledPosts: [],
56
+ },
57
+ };
58
+ const result = validateImportPayload(payload);
59
+ assert.equal(result.valid, true);
60
+ assert.equal(result.errors.length, 0);
61
+ });
62
+
63
+ it('rejects payload without version field', () => {
64
+ const result = validateImportPayload({ tables: {} } as any);
65
+ assert.equal(result.valid, false);
66
+ assert.ok(result.errors.some(e => e.includes('version')));
67
+ });
68
+
69
+ it('rejects payload with unsupported version', () => {
70
+ const result = validateImportPayload({
71
+ version: 99,
72
+ exportedAt: '2026-03-24T12:00:00.000Z',
73
+ tables: { siteSettings: [], prompts: [], scheduledPosts: [] },
74
+ });
75
+ assert.equal(result.valid, false);
76
+ assert.ok(result.errors.some(e => e.includes('version')));
77
+ });
78
+
79
+ it('rejects payload without tables field', () => {
80
+ const result = validateImportPayload({ version: 1 } as any);
81
+ assert.equal(result.valid, false);
82
+ assert.ok(result.errors.some(e => e.includes('tables')));
83
+ });
84
+
85
+ it('rejects payload with missing table keys', () => {
86
+ const result = validateImportPayload({
87
+ version: 1,
88
+ exportedAt: '2026-03-24T12:00:00.000Z',
89
+ tables: { siteSettings: [] },
90
+ } as any);
91
+ assert.equal(result.valid, false);
92
+ assert.ok(result.errors.some(e => e.includes('prompts')));
93
+ assert.ok(result.errors.some(e => e.includes('scheduledPosts')));
94
+ });
95
+
96
+ it('rejects siteSettings row missing required key field', () => {
97
+ const payload: DbTransferPayload = {
98
+ version: 1,
99
+ exportedAt: '2026-03-24T12:00:00.000Z',
100
+ tables: {
101
+ siteSettings: [{ value: 'Tech', updatedAt: '2026-03-24T12:00:00.000Z' } as any],
102
+ prompts: [],
103
+ scheduledPosts: [],
104
+ },
105
+ };
106
+ const result = validateImportPayload(payload);
107
+ assert.equal(result.valid, false);
108
+ assert.ok(result.errors.some(e => e.includes('siteSettings')));
109
+ });
110
+
111
+ it('rejects prompts row missing required id field', () => {
112
+ const payload: DbTransferPayload = {
113
+ version: 1,
114
+ exportedAt: '2026-03-24T12:00:00.000Z',
115
+ tables: {
116
+ siteSettings: [],
117
+ prompts: [{ name: 'test', category: 'c', promptText: 'p', updatedAt: '2026-03-24T12:00:00.000Z' } as any],
118
+ scheduledPosts: [],
119
+ },
120
+ };
121
+ const result = validateImportPayload(payload);
122
+ assert.equal(result.valid, false);
123
+ assert.ok(result.errors.some(e => e.includes('prompts')));
124
+ });
125
+
126
+ it('rejects scheduledPosts row missing required scheduledDate', () => {
127
+ const payload: DbTransferPayload = {
128
+ version: 1,
129
+ exportedAt: '2026-03-24T12:00:00.000Z',
130
+ tables: {
131
+ siteSettings: [],
132
+ prompts: [],
133
+ scheduledPosts: [{
134
+ id: '1', input: 'x', inputType: 'text', status: 'pending', createdAt: '2026-03-24T12:00:00.000Z',
135
+ } as any],
136
+ },
137
+ };
138
+ const result = validateImportPayload(payload);
139
+ assert.equal(result.valid, false);
140
+ assert.ok(result.errors.some(e => e.includes('scheduledPosts')));
141
+ });
142
+
143
+ it('accepts payload with empty tables (clears all data)', () => {
144
+ const payload: DbTransferPayload = {
145
+ version: 1,
146
+ exportedAt: '2026-03-24T12:00:00.000Z',
147
+ tables: {
148
+ siteSettings: [],
149
+ prompts: [],
150
+ scheduledPosts: [],
151
+ },
152
+ };
153
+ const result = validateImportPayload(payload);
154
+ assert.equal(result.valid, true);
155
+ });
156
+ });
157
+ ```
158
+
159
+ - [ ] **Step 2: Run tests to verify they fail**
160
+
161
+ Run: `npx tsx --test src/services/DbTransferService.test.ts`
162
+ Expected: FAIL — `validateImportPayload` does not exist
163
+
164
+ - [ ] **Step 3: Implement types and validation**
165
+
166
+ In `src/services/DbTransferService.ts`:
167
+
168
+ ```typescript
169
+ // @ts-ignore - resolved by Astro build pipeline
170
+ import { db, SiteSettings, Prompts, ScheduledPosts } from 'astro:db';
171
+
172
+ export interface SiteSettingRow {
173
+ key: string;
174
+ value: string;
175
+ updatedAt: string;
176
+ }
177
+
178
+ export interface PromptRow {
179
+ id: string;
180
+ name: string;
181
+ category: string;
182
+ promptText: string;
183
+ updatedAt: string;
184
+ }
185
+
186
+ export interface ScheduledPostRow {
187
+ id: string;
188
+ input: string;
189
+ inputType: string;
190
+ scheduledDate: string;
191
+ status: string;
192
+ generatedTitle?: string | null;
193
+ generatedDescription?: string | null;
194
+ generatedContent?: string | null;
195
+ generatedTags?: string | null;
196
+ generatedImageData?: string | null;
197
+ generatedImageAlt?: string | null;
198
+ publishedPath?: string | null;
199
+ createdAt: string;
200
+ }
201
+
202
+ export interface DbTransferPayload {
203
+ version: number;
204
+ exportedAt: string;
205
+ tables: {
206
+ siteSettings: SiteSettingRow[];
207
+ prompts: PromptRow[];
208
+ scheduledPosts: ScheduledPostRow[];
209
+ };
210
+ }
211
+
212
+ export interface ValidationResult {
213
+ valid: boolean;
214
+ errors: string[];
215
+ }
216
+
217
+ const SUPPORTED_VERSIONS = [1];
218
+
219
+ export function validateImportPayload(payload: unknown): ValidationResult {
220
+ const errors: string[] = [];
221
+ const data = payload as Record<string, unknown>;
222
+
223
+ if (!data || typeof data !== 'object') {
224
+ return { valid: false, errors: ['Payload must be a JSON object'] };
225
+ }
226
+
227
+ if (typeof data.version !== 'number' || !SUPPORTED_VERSIONS.includes(data.version)) {
228
+ errors.push(`Missing or unsupported "version" field (supported: ${SUPPORTED_VERSIONS.join(', ')})`);
229
+ }
230
+
231
+ if (!data.tables || typeof data.tables !== 'object') {
232
+ errors.push('Missing or invalid "tables" field');
233
+ return { valid: false, errors };
234
+ }
235
+
236
+ const tables = data.tables as Record<string, unknown>;
237
+
238
+ // Require all three table keys to be present as arrays
239
+ if (!Array.isArray(tables.siteSettings)) {
240
+ errors.push('Missing or invalid "tables.siteSettings" (must be an array)');
241
+ }
242
+ if (!Array.isArray(tables.prompts)) {
243
+ errors.push('Missing or invalid "tables.prompts" (must be an array)');
244
+ }
245
+ if (!Array.isArray(tables.scheduledPosts)) {
246
+ errors.push('Missing or invalid "tables.scheduledPosts" (must be an array)');
247
+ }
248
+
249
+ // If any table key is missing, return early — row validation won't work
250
+ if (errors.length > 0) {
251
+ return { valid: false, errors };
252
+ }
253
+
254
+ // Validate siteSettings rows
255
+ for (let i = 0; i < (tables.siteSettings as any[]).length; i++) {
256
+ const row = (tables.siteSettings as any[])[i] as Record<string, unknown>;
257
+ if (!row || typeof row.key !== 'string' || typeof row.value !== 'string') {
258
+ errors.push(`siteSettings[${i}]: missing required fields "key" and "value"`);
259
+ }
260
+ }
261
+
262
+ // Validate prompts rows
263
+ for (let i = 0; i < (tables.prompts as any[]).length; i++) {
264
+ const row = (tables.prompts as any[])[i] as Record<string, unknown>;
265
+ if (!row || typeof row.id !== 'string' || typeof row.name !== 'string' ||
266
+ typeof row.category !== 'string' || typeof row.promptText !== 'string') {
267
+ errors.push(`prompts[${i}]: missing required fields "id", "name", "category", "promptText"`);
268
+ }
269
+ }
270
+
271
+ // Validate scheduledPosts rows (all non-optional columns from schema)
272
+ for (let i = 0; i < (tables.scheduledPosts as any[]).length; i++) {
273
+ const row = (tables.scheduledPosts as any[])[i] as Record<string, unknown>;
274
+ if (!row || typeof row.id !== 'string' || typeof row.input !== 'string' ||
275
+ typeof row.inputType !== 'string' || typeof row.scheduledDate !== 'string' ||
276
+ typeof row.status !== 'string' || typeof row.createdAt !== 'string') {
277
+ errors.push(`scheduledPosts[${i}]: missing required fields "id", "input", "inputType", "scheduledDate", "status", "createdAt"`);
278
+ }
279
+ }
280
+
281
+ return { valid: errors.length === 0, errors };
282
+ }
283
+
284
+ export class DbTransferService {
285
+ async exportAll(): Promise<DbTransferPayload> {
286
+ const [siteSettings, prompts, scheduledPosts] = await Promise.all([
287
+ db.select().from(SiteSettings),
288
+ db.select().from(Prompts),
289
+ db.select().from(ScheduledPosts),
290
+ ]);
291
+
292
+ return {
293
+ version: 1,
294
+ exportedAt: new Date().toISOString(),
295
+ tables: {
296
+ siteSettings: siteSettings.map((row: any) => ({
297
+ key: row.key,
298
+ value: row.value,
299
+ updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : String(row.updatedAt),
300
+ })),
301
+ prompts: prompts.map((row: any) => ({
302
+ id: row.id,
303
+ name: row.name,
304
+ category: row.category,
305
+ promptText: row.promptText,
306
+ updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : String(row.updatedAt),
307
+ })),
308
+ scheduledPosts: scheduledPosts.map((row: any) => ({
309
+ id: row.id,
310
+ input: row.input,
311
+ inputType: row.inputType,
312
+ scheduledDate: row.scheduledDate instanceof Date ? row.scheduledDate.toISOString() : String(row.scheduledDate),
313
+ status: row.status,
314
+ generatedTitle: row.generatedTitle ?? null,
315
+ generatedDescription: row.generatedDescription ?? null,
316
+ generatedContent: row.generatedContent ?? null,
317
+ generatedTags: row.generatedTags ?? null,
318
+ generatedImageData: row.generatedImageData ?? null,
319
+ generatedImageAlt: row.generatedImageAlt ?? null,
320
+ publishedPath: row.publishedPath ?? null,
321
+ createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),
322
+ })),
323
+ },
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Replace all content data atomically using db.batch().
329
+ * Deletes all existing rows in each table, then inserts the imported rows.
330
+ * Sessions table is untouched — user stays logged in.
331
+ */
332
+ async importAll(payload: DbTransferPayload): Promise<{ imported: Record<string, number> }> {
333
+ const { siteSettings, prompts, scheduledPosts } = payload.tables;
334
+
335
+ // Build all statements for atomic batch execution
336
+ const statements: any[] = [];
337
+
338
+ // Delete all existing rows from content tables
339
+ statements.push(db.delete(SiteSettings));
340
+ statements.push(db.delete(Prompts));
341
+ statements.push(db.delete(ScheduledPosts));
342
+
343
+ // Insert new SiteSettings
344
+ for (const row of siteSettings) {
345
+ statements.push(
346
+ db.insert(SiteSettings).values({
347
+ key: row.key,
348
+ value: row.value,
349
+ updatedAt: new Date(row.updatedAt),
350
+ })
351
+ );
352
+ }
353
+
354
+ // Insert new Prompts
355
+ for (const row of prompts) {
356
+ statements.push(
357
+ db.insert(Prompts).values({
358
+ id: row.id,
359
+ name: row.name,
360
+ category: row.category,
361
+ promptText: row.promptText,
362
+ updatedAt: new Date(row.updatedAt),
363
+ })
364
+ );
365
+ }
366
+
367
+ // Insert new ScheduledPosts
368
+ for (const row of scheduledPosts) {
369
+ statements.push(
370
+ db.insert(ScheduledPosts).values({
371
+ id: row.id,
372
+ input: row.input,
373
+ inputType: row.inputType,
374
+ scheduledDate: new Date(row.scheduledDate),
375
+ status: row.status,
376
+ generatedTitle: row.generatedTitle ?? undefined,
377
+ generatedDescription: row.generatedDescription ?? undefined,
378
+ generatedContent: row.generatedContent ?? undefined,
379
+ generatedTags: row.generatedTags ?? undefined,
380
+ generatedImageData: row.generatedImageData ?? undefined,
381
+ generatedImageAlt: row.generatedImageAlt ?? undefined,
382
+ publishedPath: row.publishedPath ?? undefined,
383
+ createdAt: new Date(row.createdAt),
384
+ })
385
+ );
386
+ }
387
+
388
+ // Execute all statements atomically
389
+ await db.batch(statements);
390
+
391
+ return {
392
+ imported: {
393
+ siteSettings: siteSettings.length,
394
+ prompts: prompts.length,
395
+ scheduledPosts: scheduledPosts.length,
396
+ },
397
+ };
398
+ }
399
+ }
400
+ ```
401
+
402
+ - [ ] **Step 4: Run tests to verify validation passes**
403
+
404
+ Run: `npx tsx --test src/services/DbTransferService.test.ts`
405
+ Expected: PASS — all 9 validation tests green
406
+
407
+ - [ ] **Step 5: Commit**
408
+
409
+ ```bash
410
+ git add src/services/DbTransferService.ts src/services/DbTransferService.test.ts
411
+ git commit -m "feat: add DbTransferService with JSON export/import and validation"
412
+ ```
413
+
414
+ ---
415
+
416
+ ### Task 2: JSON Export Endpoint
417
+
418
+ **Files:**
419
+ - Create: `src/api/db/export.ts`
420
+
421
+ - [ ] **Step 1: Write the export endpoint**
422
+
423
+ In `src/api/db/export.ts`:
424
+
425
+ ```typescript
426
+ import type { APIRoute } from 'astro';
427
+ import { jsonError } from '../_utils';
428
+ import { DbTransferService } from '../../services/DbTransferService.js';
429
+
430
+ export const GET: APIRoute = async () => {
431
+ try {
432
+ const service = new DbTransferService();
433
+ const payload = await service.exportAll();
434
+ const json = JSON.stringify(payload, null, 2);
435
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
436
+
437
+ return new Response(json, {
438
+ status: 200,
439
+ headers: {
440
+ 'Content-Type': 'application/json',
441
+ 'Content-Disposition': `attachment; filename="content-${timestamp}.json"`,
442
+ 'Content-Length': String(Buffer.byteLength(json)),
443
+ },
444
+ });
445
+ } catch {
446
+ return jsonError('Database export failed', 500);
447
+ }
448
+ };
449
+ ```
450
+
451
+ - [ ] **Step 2: Commit**
452
+
453
+ ```bash
454
+ git add src/api/db/export.ts
455
+ git commit -m "feat: add GET /api/db/export endpoint for JSON data export"
456
+ ```
457
+
458
+ ---
459
+
460
+ ### Task 3: JSON Import Endpoint
461
+
462
+ **Files:**
463
+ - Create: `src/api/db/import.ts`
464
+
465
+ - [ ] **Step 1: Write the import endpoint**
466
+
467
+ In `src/api/db/import.ts`:
468
+
469
+ ```typescript
470
+ import type { APIRoute } from 'astro';
471
+ import { jsonResponse, jsonError } from '../_utils';
472
+ import { DbTransferService, validateImportPayload } from '../../services/DbTransferService.js';
473
+
474
+ const MAX_IMPORT_SIZE = 50 * 1024 * 1024; // 50 MB
475
+
476
+ export const POST: APIRoute = async ({ request }) => {
477
+ try {
478
+ const contentType = request.headers.get('content-type') || '';
479
+ if (!contentType.includes('application/json')) {
480
+ return jsonError('Invalid content type. Use application/json.', 400);
481
+ }
482
+
483
+ const contentLength = Number(request.headers.get('content-length') || 0);
484
+ if (contentLength > MAX_IMPORT_SIZE) {
485
+ return jsonError('Payload too large. Maximum 50 MB.', 413);
486
+ }
487
+
488
+ const text = await request.text();
489
+ if (text.length > MAX_IMPORT_SIZE) {
490
+ return jsonError('Payload too large. Maximum 50 MB.', 413);
491
+ }
492
+
493
+ let payload: unknown;
494
+ try {
495
+ payload = JSON.parse(text);
496
+ } catch {
497
+ return jsonError('Invalid JSON.', 400);
498
+ }
499
+
500
+ const validation = validateImportPayload(payload);
501
+ if (!validation.valid) {
502
+ return jsonError(`Validation failed: ${validation.errors.join('; ')}`, 400);
503
+ }
504
+
505
+ const service = new DbTransferService();
506
+ const result = await service.importAll(payload as any);
507
+
508
+ return jsonResponse({
509
+ success: true,
510
+ imported: result.imported,
511
+ });
512
+ } catch {
513
+ return jsonError('Database import failed', 500);
514
+ }
515
+ };
516
+ ```
517
+
518
+ - [ ] **Step 2: Commit**
519
+
520
+ ```bash
521
+ git add src/api/db/import.ts
522
+ git commit -m "feat: add POST /api/db/import endpoint for JSON data import"
523
+ ```
524
+
525
+ ---
526
+
527
+ ### Task 4: Manual Integration Test
528
+
529
+ **Files:**
530
+ - No new files — testing against running dev server
531
+
532
+ - [ ] **Step 1: Start dev server and test the full round-trip**
533
+
534
+ ```bash
535
+ # Terminal 1: start dev server
536
+ npm run dev
537
+
538
+ # Terminal 2: export current data
539
+ curl -s http://localhost:4321/api/db/export | jq '.tables | keys'
540
+ # Expected: ["prompts", "scheduledPosts", "siteSettings"]
541
+
542
+ # Save export to file
543
+ curl -s http://localhost:4321/api/db/export > /tmp/db-export.json
544
+
545
+ # Import it back (should replace without errors)
546
+ curl -X POST http://localhost:4321/api/db/import \
547
+ -H 'Content-Type: application/json' \
548
+ -d @/tmp/db-export.json
549
+ # Expected: {"success": true, "imported": {"siteSettings": N, "prompts": N, "scheduledPosts": N}}
550
+ ```
551
+
552
+ - [ ] **Step 2: Verify no redirect loop — session stays valid**
553
+
554
+ After import, navigate to `/editor` in browser. Confirm:
555
+ - No redirect loop
556
+ - Settings are present and correct
557
+ - Session is still valid (you're still logged in)
558
+
559
+ - [ ] **Step 3: Test validation errors**
560
+
561
+ ```bash
562
+ # Missing version
563
+ curl -X POST http://localhost:4321/api/db/import \
564
+ -H 'Content-Type: application/json' \
565
+ -d '{"tables": {}}'
566
+ # Expected: 400 with validation error about "version"
567
+
568
+ # Missing table keys
569
+ curl -X POST http://localhost:4321/api/db/import \
570
+ -H 'Content-Type: application/json' \
571
+ -d '{"version": 1, "tables": {"siteSettings": []}}'
572
+ # Expected: 400 with validation error about missing "prompts" and "scheduledPosts"
573
+
574
+ # Invalid JSON
575
+ curl -X POST http://localhost:4321/api/db/import \
576
+ -H 'Content-Type: application/json' \
577
+ -d 'not json'
578
+ # Expected: 400 "Invalid JSON"
579
+ ```
580
+
581
+ - [ ] **Step 4: Commit any fixes from integration testing**
582
+
583
+ ```bash
584
+ git add -A
585
+ git commit -m "fix: adjustments from integration testing"
586
+ ```
587
+
588
+ ---
589
+
590
+ ### Task 5: Deprecate Old SQLite Upload
591
+
592
+ **Files:**
593
+ - Modify: `src/api/db/upload.ts:56-60` — add deprecation notice in response
594
+
595
+ - [ ] **Step 1: Add deprecation message to the SQLite upload response**
596
+
597
+ In `src/api/db/upload.ts`, update the success response:
598
+
599
+ ```typescript
600
+ return jsonResponse({
601
+ success: true,
602
+ size: dbBuffer.length,
603
+ message: 'Database uploaded. Server restarting...',
604
+ deprecated: 'Use POST /api/db/import with JSON payload instead. The SQLite file upload causes connection issues and will be removed in a future version.',
605
+ });
606
+ ```
607
+
608
+ - [ ] **Step 2: Commit**
609
+
610
+ ```bash
611
+ git add src/api/db/upload.ts
612
+ git commit -m "chore: add deprecation notice to SQLite file upload endpoint"
613
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nca-ai-cms-astro-plugin",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -0,0 +1,23 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { jsonError } from '../_utils';
3
+ import { DbTransferService } from '../../services/DbTransferService.js';
4
+
5
+ export const GET: APIRoute = async () => {
6
+ try {
7
+ const service = new DbTransferService();
8
+ const payload = await service.exportAll();
9
+ const json = JSON.stringify(payload, null, 2);
10
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
11
+
12
+ return new Response(json, {
13
+ status: 200,
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ 'Content-Disposition': `attachment; filename="content-${timestamp}.json"`,
17
+ 'Content-Length': String(Buffer.byteLength(json)),
18
+ },
19
+ });
20
+ } catch {
21
+ return jsonError('Database export failed', 500);
22
+ }
23
+ };
@@ -0,0 +1,46 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { jsonResponse, jsonError } from '../_utils';
3
+ import { DbTransferService, validateImportPayload } from '../../services/DbTransferService.js';
4
+
5
+ const MAX_IMPORT_SIZE = 50 * 1024 * 1024; // 50 MB
6
+
7
+ export const POST: APIRoute = async ({ request }) => {
8
+ try {
9
+ const contentType = request.headers.get('content-type') || '';
10
+ if (!contentType.includes('application/json')) {
11
+ return jsonError('Invalid content type. Use application/json.', 400);
12
+ }
13
+
14
+ const contentLength = Number(request.headers.get('content-length') || 0);
15
+ if (contentLength > MAX_IMPORT_SIZE) {
16
+ return jsonError('Payload too large. Maximum 50 MB.', 413);
17
+ }
18
+
19
+ const text = await request.text();
20
+ if (text.length > MAX_IMPORT_SIZE) {
21
+ return jsonError('Payload too large. Maximum 50 MB.', 413);
22
+ }
23
+
24
+ let payload: unknown;
25
+ try {
26
+ payload = JSON.parse(text);
27
+ } catch {
28
+ return jsonError('Invalid JSON.', 400);
29
+ }
30
+
31
+ const validation = validateImportPayload(payload);
32
+ if (!validation.valid) {
33
+ return jsonError(`Validation failed: ${validation.errors.join('; ')}`, 400);
34
+ }
35
+
36
+ const service = new DbTransferService();
37
+ const result = await service.importAll(payload as any);
38
+
39
+ return jsonResponse({
40
+ success: true,
41
+ imported: result.imported,
42
+ });
43
+ } catch {
44
+ return jsonError('Database import failed', 500);
45
+ }
46
+ };