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

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/README.md CHANGED
@@ -85,6 +85,9 @@ ncaAiCms({
85
85
  | `/api/prompts` | Manage prompt templates |
86
86
  | `/api/scheduler` | Manage scheduled posts |
87
87
  | `/api/articles/*` | Article operations |
88
+ | `/api/db/export` | Export settings + prompts as JSON |
89
+ | `/api/db/import` | Import settings + prompts (merge/upsert) |
90
+ | `/api/db/download` | Download raw SQLite database backup |
88
91
 
89
92
  All `/api/*` and `/editor` routes are protected by cookie-based authentication.
90
93
 
@@ -0,0 +1,461 @@
1
+ # Settings 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:** Refactor the database import/export into a community-friendly settings transfer system with merge (upsert) semantics and partial import support — no data loss, no connection issues, no restart needed.
6
+
7
+ **Architecture:** Rework `DbTransferService` to export only shareable configuration (SiteSettings + Prompts, not ScheduledPosts). Flatten the payload (no `tables` wrapper). Import uses upsert semantics (merge into existing data) instead of delete-and-replace. Payload sections are optional so users can import just prompts or just settings. Clean up dead code from removed SQLite upload.
8
+
9
+ **Tech Stack:** Astro API routes, `astro:db` (existing), vitest, no new dependencies.
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | Action | File | Responsibility |
16
+ |--------|------|----------------|
17
+ | Modify | `src/services/DbTransferService.ts` | Drop ScheduledPosts, flatten payload, upsert semantics, partial import |
18
+ | Modify | `src/services/DbTransferService.test.ts` | Validation tests for new flat schema with partial import |
19
+ | Modify | `src/services/index.ts` | Drop `ScheduledPostRow` export |
20
+ | Rewrite | `src/api/db/upload.test.ts` | Replace stale tests with single 410 stub test |
21
+ | Delete | `src/utils/dbUploadUtils.ts` | Dead code — upload is a 410 stub |
22
+ | Delete | `src/utils/dbUploadUtils.test.ts` | Tests for dead code |
23
+ | Modify | `src/utils/index.ts` | Remove dbUploadUtils exports |
24
+ | Keep | `src/api/db/export.ts` | No changes (calls `exportAll`) |
25
+ | Keep | `src/api/db/import.ts` | No changes (calls `importAll`) |
26
+ | Keep | `src/api/db/upload.ts` | Already a 410 stub |
27
+ | Keep | `src/api/db/download.ts` | Raw SQLite backup stays |
28
+
29
+ **What's exported/imported (flat payload):**
30
+ - `siteSettings` — site configuration (content settings, image settings, CTA config)
31
+ - `prompts` — AI prompt templates (community-shareable)
32
+
33
+ **What's removed:**
34
+ - `scheduledPosts` — instance-specific runtime state
35
+ - `tables` wrapper — payload is now flat (`{ version, siteSettings, prompts }`)
36
+
37
+ **Import semantics:**
38
+ - **Upsert/merge** — existing rows updated by key/id, new rows inserted, unmentioned rows untouched
39
+ - **Partial** — include only the sections you want to update
40
+
41
+ **Upsert pattern:** Uses `.get()` + update-or-insert (same pattern as `PromptService.updateSetting` at `src/services/PromptService.ts:124-143`). This is the established codebase convention — `.get()` is used across `SessionService`, `PromptService`, and `SchedulerDBAdapter`.
42
+
43
+ ---
44
+
45
+ ### Task 1: Rewrite DbTransferService — Flat Payload, Upsert, Partial Import
46
+
47
+ **Files:**
48
+ - Modify: `src/services/DbTransferService.ts`
49
+ - Modify: `src/services/DbTransferService.test.ts`
50
+
51
+ - [ ] **Step 1: Update validation tests for new schema**
52
+
53
+ Replace the full contents of `src/services/DbTransferService.test.ts`:
54
+
55
+ ```typescript
56
+ import { describe, it, expect, vi } from 'vitest';
57
+
58
+ vi.mock('astro:db', () => ({
59
+ db: {},
60
+ SiteSettings: {},
61
+ Prompts: {},
62
+ eq: vi.fn(),
63
+ }));
64
+
65
+ import { validateImportPayload, type DbTransferPayload } from './DbTransferService.js';
66
+
67
+ describe('validateImportPayload', () => {
68
+ it('accepts a valid payload with both sections', () => {
69
+ const payload: DbTransferPayload = {
70
+ version: 1,
71
+ exportedAt: '2026-03-24T12:00:00.000Z',
72
+ siteSettings: [{ key: 'content.branche', value: 'Tech', updatedAt: '2026-03-24T12:00:00.000Z' }],
73
+ prompts: [{ id: 'p1', name: 'Blog', category: 'content', promptText: 'Write...', updatedAt: '2026-03-24T12:00:00.000Z' }],
74
+ };
75
+ const result = validateImportPayload(payload);
76
+ expect(result.valid).toBe(true);
77
+ expect(result.errors).toHaveLength(0);
78
+ });
79
+
80
+ it('accepts payload with only siteSettings (partial import)', () => {
81
+ const result = validateImportPayload({
82
+ version: 1,
83
+ exportedAt: '2026-03-24T12:00:00.000Z',
84
+ siteSettings: [{ key: 'k', value: 'v', updatedAt: '2026-03-24T12:00:00.000Z' }],
85
+ });
86
+ expect(result.valid).toBe(true);
87
+ });
88
+
89
+ it('accepts payload with only prompts (partial import)', () => {
90
+ const result = validateImportPayload({
91
+ version: 1,
92
+ exportedAt: '2026-03-24T12:00:00.000Z',
93
+ prompts: [{ id: 'p1', name: 'n', category: 'c', promptText: 't', updatedAt: '2026-03-24T12:00:00.000Z' }],
94
+ });
95
+ expect(result.valid).toBe(true);
96
+ });
97
+
98
+ it('rejects payload with neither siteSettings nor prompts', () => {
99
+ const result = validateImportPayload({ version: 1, exportedAt: '2026-03-24T12:00:00.000Z' });
100
+ expect(result.valid).toBe(false);
101
+ expect(result.errors.some(e => e.includes('at least one'))).toBe(true);
102
+ });
103
+
104
+ it('rejects payload without version field', () => {
105
+ const result = validateImportPayload({ siteSettings: [] });
106
+ expect(result.valid).toBe(false);
107
+ expect(result.errors.some(e => e.includes('version'))).toBe(true);
108
+ });
109
+
110
+ it('rejects unsupported version', () => {
111
+ const result = validateImportPayload({
112
+ version: 99,
113
+ exportedAt: '2026-03-24T12:00:00.000Z',
114
+ siteSettings: [],
115
+ });
116
+ expect(result.valid).toBe(false);
117
+ expect(result.errors.some(e => e.includes('version'))).toBe(true);
118
+ });
119
+
120
+ it('rejects siteSettings row missing key', () => {
121
+ const result = validateImportPayload({
122
+ version: 1,
123
+ exportedAt: '2026-03-24T12:00:00.000Z',
124
+ siteSettings: [{ value: 'v', updatedAt: '2026-03-24T12:00:00.000Z' }],
125
+ });
126
+ expect(result.valid).toBe(false);
127
+ expect(result.errors.some(e => e.includes('siteSettings'))).toBe(true);
128
+ });
129
+
130
+ it('rejects prompts row missing id', () => {
131
+ const result = validateImportPayload({
132
+ version: 1,
133
+ exportedAt: '2026-03-24T12:00:00.000Z',
134
+ prompts: [{ name: 'n', category: 'c', promptText: 't', updatedAt: '2026-03-24T12:00:00.000Z' }],
135
+ });
136
+ expect(result.valid).toBe(false);
137
+ expect(result.errors.some(e => e.includes('prompts'))).toBe(true);
138
+ });
139
+
140
+ it('accepts empty arrays (valid but no-op)', () => {
141
+ const result = validateImportPayload({
142
+ version: 1,
143
+ exportedAt: '2026-03-24T12:00:00.000Z',
144
+ siteSettings: [],
145
+ prompts: [],
146
+ });
147
+ expect(result.valid).toBe(true);
148
+ });
149
+
150
+ it('rejects non-array siteSettings', () => {
151
+ const result = validateImportPayload({
152
+ version: 1,
153
+ exportedAt: '2026-03-24T12:00:00.000Z',
154
+ siteSettings: 'not an array',
155
+ });
156
+ expect(result.valid).toBe(false);
157
+ expect(result.errors.some(e => e.includes('siteSettings'))).toBe(true);
158
+ });
159
+ });
160
+ ```
161
+
162
+ - [ ] **Step 2: Run tests to verify they fail**
163
+
164
+ Run: `npx vitest run src/services/DbTransferService.test.ts`
165
+ Expected: FAIL — old `DbTransferPayload` has `tables` property, not flat `siteSettings`/`prompts`
166
+
167
+ - [ ] **Step 3: Rewrite DbTransferService**
168
+
169
+ Replace `src/services/DbTransferService.ts` with:
170
+
171
+ ```typescript
172
+ // @ts-ignore - resolved by Astro build pipeline
173
+ import { db, SiteSettings, Prompts, eq } from 'astro:db';
174
+
175
+ export interface SiteSettingRow {
176
+ key: string;
177
+ value: string;
178
+ updatedAt: string;
179
+ }
180
+
181
+ export interface PromptRow {
182
+ id: string;
183
+ name: string;
184
+ category: string;
185
+ promptText: string;
186
+ updatedAt: string;
187
+ }
188
+
189
+ export interface DbTransferPayload {
190
+ version: number;
191
+ exportedAt: string;
192
+ siteSettings?: SiteSettingRow[];
193
+ prompts?: PromptRow[];
194
+ }
195
+
196
+ export interface ValidationResult {
197
+ valid: boolean;
198
+ errors: string[];
199
+ }
200
+
201
+ const SUPPORTED_VERSIONS = [1];
202
+
203
+ export function validateImportPayload(payload: unknown): ValidationResult {
204
+ const errors: string[] = [];
205
+ const data = payload as Record<string, unknown>;
206
+
207
+ if (!data || typeof data !== 'object') {
208
+ return { valid: false, errors: ['Payload must be a JSON object'] };
209
+ }
210
+
211
+ if (typeof data.version !== 'number' || !SUPPORTED_VERSIONS.includes(data.version)) {
212
+ errors.push(`Missing or unsupported "version" field (supported: ${SUPPORTED_VERSIONS.join(', ')})`);
213
+ }
214
+
215
+ const hasSiteSettings = 'siteSettings' in data;
216
+ const hasPrompts = 'prompts' in data;
217
+
218
+ if (!hasSiteSettings && !hasPrompts) {
219
+ errors.push('Payload must contain at least one of "siteSettings" or "prompts"');
220
+ return { valid: false, errors };
221
+ }
222
+
223
+ if (hasSiteSettings) {
224
+ if (!Array.isArray(data.siteSettings)) {
225
+ errors.push('"siteSettings" must be an array');
226
+ } else {
227
+ for (let i = 0; i < data.siteSettings.length; i++) {
228
+ const row = data.siteSettings[i] as Record<string, unknown>;
229
+ if (!row || typeof row.key !== 'string' || typeof row.value !== 'string') {
230
+ errors.push(`siteSettings[${i}]: missing required fields "key" and "value"`);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ if (hasPrompts) {
237
+ if (!Array.isArray(data.prompts)) {
238
+ errors.push('"prompts" must be an array');
239
+ } else {
240
+ for (let i = 0; i < data.prompts.length; i++) {
241
+ const row = data.prompts[i] as Record<string, unknown>;
242
+ if (!row || typeof row.id !== 'string' || typeof row.name !== 'string' ||
243
+ typeof row.category !== 'string' || typeof row.promptText !== 'string') {
244
+ errors.push(`prompts[${i}]: missing required fields "id", "name", "category", "promptText"`);
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ return { valid: errors.length === 0, errors };
251
+ }
252
+
253
+ export class DbTransferService {
254
+ async exportAll(): Promise<DbTransferPayload> {
255
+ const [siteSettings, prompts] = await Promise.all([
256
+ db.select().from(SiteSettings),
257
+ db.select().from(Prompts),
258
+ ]);
259
+
260
+ return {
261
+ version: 1,
262
+ exportedAt: new Date().toISOString(),
263
+ siteSettings: siteSettings.map((row: any) => ({
264
+ key: row.key,
265
+ value: row.value,
266
+ updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : String(row.updatedAt),
267
+ })),
268
+ prompts: prompts.map((row: any) => ({
269
+ id: row.id,
270
+ name: row.name,
271
+ category: row.category,
272
+ promptText: row.promptText,
273
+ updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : String(row.updatedAt),
274
+ })),
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Merge imported data into the live database using upsert semantics.
280
+ * Only sections present in the payload are touched. Existing data not
281
+ * referenced in the payload is left untouched.
282
+ * Uses the same select-then-update-or-insert pattern as PromptService.updateSetting.
283
+ */
284
+ async importAll(payload: DbTransferPayload): Promise<{ imported: Record<string, number> }> {
285
+ const counts: Record<string, number> = {};
286
+
287
+ if (payload.siteSettings) {
288
+ for (const row of payload.siteSettings) {
289
+ const existing = await db.select().from(SiteSettings)
290
+ .where(eq(SiteSettings.key, row.key)).get();
291
+ if (existing) {
292
+ await db.update(SiteSettings)
293
+ .set({ value: row.value, updatedAt: new Date(row.updatedAt) })
294
+ .where(eq(SiteSettings.key, row.key));
295
+ } else {
296
+ await db.insert(SiteSettings).values({
297
+ key: row.key,
298
+ value: row.value,
299
+ updatedAt: new Date(row.updatedAt),
300
+ });
301
+ }
302
+ }
303
+ counts.siteSettings = payload.siteSettings.length;
304
+ }
305
+
306
+ if (payload.prompts) {
307
+ for (const row of payload.prompts) {
308
+ const existing = await db.select().from(Prompts)
309
+ .where(eq(Prompts.id, row.id)).get();
310
+ if (existing) {
311
+ await db.update(Prompts)
312
+ .set({
313
+ name: row.name,
314
+ category: row.category,
315
+ promptText: row.promptText,
316
+ updatedAt: new Date(row.updatedAt),
317
+ })
318
+ .where(eq(Prompts.id, row.id));
319
+ } else {
320
+ await db.insert(Prompts).values({
321
+ id: row.id,
322
+ name: row.name,
323
+ category: row.category,
324
+ promptText: row.promptText,
325
+ updatedAt: new Date(row.updatedAt),
326
+ });
327
+ }
328
+ }
329
+ counts.prompts = payload.prompts.length;
330
+ }
331
+
332
+ return { imported: counts };
333
+ }
334
+ }
335
+ ```
336
+
337
+ - [ ] **Step 4: Run tests to verify they pass**
338
+
339
+ Run: `npx vitest run src/services/DbTransferService.test.ts`
340
+ Expected: PASS — all 10 tests green
341
+
342
+ - [ ] **Step 5: Commit**
343
+
344
+ ```bash
345
+ git add src/services/DbTransferService.ts src/services/DbTransferService.test.ts
346
+ git commit -m "refactor: settings-focused export/import with upsert and partial import"
347
+ ```
348
+
349
+ ---
350
+
351
+ ### Task 2: Fix Stale Upload Test and Clean Up Dead Code
352
+
353
+ **Files:**
354
+ - Rewrite: `src/api/db/upload.test.ts`
355
+ - Delete: `src/utils/dbUploadUtils.ts`
356
+ - Delete: `src/utils/dbUploadUtils.test.ts`
357
+ - Modify: `src/utils/index.ts:8-11` — remove dbUploadUtils exports
358
+ - Modify: `src/services/index.ts:28-36` — remove `ScheduledPostRow`
359
+
360
+ - [ ] **Step 1: Replace the upload test**
361
+
362
+ Replace `src/api/db/upload.test.ts` with:
363
+
364
+ ```typescript
365
+ import { describe, it, expect } from 'vitest';
366
+
367
+ describe('DB Upload API (deprecated)', () => {
368
+ it('returns 410 Gone with migration instructions', async () => {
369
+ const { POST } = await import('./upload.js');
370
+
371
+ const response = await POST({ request: new Request('http://localhost/api/db/upload', { method: 'POST' }) } as any);
372
+ const data = await response.json();
373
+
374
+ expect(response.status).toBe(410);
375
+ expect(data.error).toContain('/api/db/import');
376
+ });
377
+ });
378
+ ```
379
+
380
+ - [ ] **Step 2: Delete dead dbUploadUtils files**
381
+
382
+ ```bash
383
+ rm src/utils/dbUploadUtils.ts src/utils/dbUploadUtils.test.ts
384
+ ```
385
+
386
+ - [ ] **Step 3: Remove dbUploadUtils exports from barrel**
387
+
388
+ In `src/utils/index.ts`, remove:
389
+ ```typescript
390
+ export {
391
+ validateSqliteHeader,
392
+ validateFileSize,
393
+ } from "./dbUploadUtils.js";
394
+ ```
395
+
396
+ The file should become:
397
+ ```typescript
398
+ export { renderMarkdown } from "./markdown.js";
399
+ export {
400
+ sanitizeMarkdownHtml,
401
+ escapeJsonLd,
402
+ escapeHtml,
403
+ } from "./sanitize.js";
404
+ export { getEnvVariable } from "./envUtils.js";
405
+ ```
406
+
407
+ - [ ] **Step 4: Update services barrel — remove ScheduledPostRow**
408
+
409
+ In `src/services/index.ts`, replace the DbTransferService export block with:
410
+
411
+ ```typescript
412
+ export {
413
+ DbTransferService,
414
+ validateImportPayload,
415
+ type DbTransferPayload,
416
+ type ValidationResult,
417
+ type SiteSettingRow,
418
+ type PromptRow,
419
+ } from './DbTransferService';
420
+ ```
421
+
422
+ - [ ] **Step 5: Run upload test**
423
+
424
+ Run: `npx vitest run src/api/db/upload.test.ts`
425
+ Expected: PASS — 1 test green
426
+
427
+ - [ ] **Step 6: Commit**
428
+
429
+ ```bash
430
+ git add -A
431
+ git commit -m "chore: fix stale upload test, remove dead dbUploadUtils code"
432
+ ```
433
+
434
+ ---
435
+
436
+ ### Task 3: Run Full Test Suite and Verify
437
+
438
+ **Files:**
439
+ - No new files — verification only
440
+
441
+ - [ ] **Step 1: Run all tests**
442
+
443
+ Run: `npx vitest run`
444
+ Expected: ALL tests pass, zero failures
445
+
446
+ - [ ] **Step 2: Check for stale references**
447
+
448
+ ```bash
449
+ grep -r "ScheduledPostRow" src/ --include="*.ts" | grep -v node_modules
450
+ grep -r "dbUploadUtils" src/ --include="*.ts" | grep -v node_modules
451
+ grep -r "tables\." src/services/DbTransferService.ts
452
+ ```
453
+
454
+ Expected: No results for any of these (all old references removed)
455
+
456
+ - [ ] **Step 3: Commit any fixes if needed**
457
+
458
+ ```bash
459
+ git add -A
460
+ git commit -m "fix: adjustments from verification"
461
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nca-ai-cms-astro-plugin",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -1,173 +1,13 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import * as fs from 'fs/promises';
1
+ import { describe, it, expect } from 'vitest';
3
2
 
4
- const mockClose = vi.fn();
5
- const mockReconnect = vi.fn();
6
-
7
- vi.mock('astro:db', () => ({
8
- db: {
9
- $client: {
10
- close: mockClose,
11
- reconnect: mockReconnect,
12
- },
13
- },
14
- }));
15
-
16
- vi.mock('fs/promises', () => ({
17
- writeFile: vi.fn().mockResolvedValue(undefined),
18
- copyFile: vi.fn().mockResolvedValue(undefined),
19
- }));
20
-
21
- // SQLite file header
22
- const SQLITE_HEADER = Buffer.from('SQLite format 3\0');
23
-
24
- function createSqliteBuffer(size = 4096): Buffer {
25
- const buf = Buffer.alloc(size);
26
- SQLITE_HEADER.copy(buf);
27
- return buf;
28
- }
29
-
30
- function createFormDataRequest(file: Buffer, fieldName = 'database'): Request {
31
- const formData = new FormData();
32
- formData.append(fieldName, new Blob([file]), 'test.db');
33
- return new Request('http://localhost/api/db/upload', {
34
- method: 'POST',
35
- body: formData,
36
- });
37
- }
38
-
39
- function createOctetStreamRequest(file: Buffer): Request {
40
- return new Request('http://localhost/api/db/upload', {
41
- method: 'POST',
42
- headers: { 'Content-Type': 'application/octet-stream' },
43
- body: file,
44
- });
45
- }
46
-
47
- describe('DB Upload API', () => {
48
- beforeEach(() => {
49
- vi.clearAllMocks();
50
- process.env.ASTRO_DATABASE_FILE = '.astro/content.db';
51
- });
52
-
53
- it('accepts a valid SQLite file via multipart/form-data', async () => {
54
- const { POST } = await import('./upload.js');
55
- const request = createFormDataRequest(createSqliteBuffer());
56
-
57
- const response = await POST({ request } as any);
58
- const data = await response.json();
59
-
60
- expect(response.status).toBe(200);
61
- expect(data.success).toBe(true);
62
- expect(data.size).toBeGreaterThan(0);
63
- });
64
-
65
- it('accepts a valid SQLite file via application/octet-stream', async () => {
66
- const { POST } = await import('./upload.js');
67
- const request = createOctetStreamRequest(createSqliteBuffer());
68
-
69
- const response = await POST({ request } as any);
70
- const data = await response.json();
71
-
72
- expect(response.status).toBe(200);
73
- expect(data.success).toBe(true);
74
- });
75
-
76
- it('rejects non-SQLite files', async () => {
77
- const { POST } = await import('./upload.js');
78
- const badFile = Buffer.from('not a sqlite file');
79
- const request = createFormDataRequest(badFile);
80
-
81
- const response = await POST({ request } as any);
82
- const data = await response.json();
83
-
84
- expect(response.status).toBe(400);
85
- expect(data.error).toContain('not a SQLite database');
86
- });
87
-
88
- it('rejects missing database field', async () => {
89
- const { POST } = await import('./upload.js');
90
- const formData = new FormData();
91
- formData.append('wrongfield', new Blob([createSqliteBuffer()]), 'test.db');
92
- const request = new Request('http://localhost/api/db/upload', {
93
- method: 'POST',
94
- body: formData,
95
- });
96
-
97
- const response = await POST({ request } as any);
98
- const data = await response.json();
99
-
100
- expect(response.status).toBe(400);
101
- expect(data.error).toContain('No database file');
102
- });
103
-
104
- it('rejects invalid content type', async () => {
105
- const { POST } = await import('./upload.js');
106
- const request = new Request('http://localhost/api/db/upload', {
107
- method: 'POST',
108
- headers: { 'Content-Type': 'text/plain' },
109
- body: 'hello',
110
- });
111
-
112
- const response = await POST({ request } as any);
113
- const data = await response.json();
114
-
115
- expect(response.status).toBe(400);
116
- expect(data.error).toContain('Invalid content type');
117
- });
118
-
119
- it('creates a backup before writing', async () => {
120
- const { POST } = await import('./upload.js');
121
- const request = createFormDataRequest(createSqliteBuffer());
122
-
123
- await POST({ request } as any);
124
-
125
- expect(fs.copyFile).toHaveBeenCalled();
126
- });
127
-
128
- it('reconnects the DB client after successful upload', async () => {
129
- const { POST } = await import('./upload.js');
130
- const request = createFormDataRequest(createSqliteBuffer());
131
-
132
- await POST({ request } as any);
133
-
134
- expect(mockClose).toHaveBeenCalledOnce();
135
- expect(mockReconnect).toHaveBeenCalledOnce();
136
- });
137
-
138
- it('rejects oversized file via multipart/form-data', async () => {
139
- const { POST } = await import('./upload.js');
140
- const oversized = createSqliteBuffer(50 * 1024 * 1024 + 1);
141
- const request = createFormDataRequest(oversized);
142
-
143
- const response = await POST({ request } as any);
144
- const data = await response.json();
145
-
146
- expect(response.status).toBe(413);
147
- expect(data.error).toContain('too large');
148
- });
149
-
150
- it('rejects oversized file via application/octet-stream', async () => {
151
- const { POST } = await import('./upload.js');
152
- const oversized = createSqliteBuffer(50 * 1024 * 1024 + 1);
153
- const request = createOctetStreamRequest(oversized);
154
-
155
- const response = await POST({ request } as any);
156
- const data = await response.json();
157
-
158
- expect(response.status).toBe(413);
159
- expect(data.error).toContain('too large');
160
- });
161
-
162
- it('still succeeds if reconnect is not available', async () => {
3
+ describe('DB Upload API (deprecated)', () => {
4
+ it('returns 410 Gone with migration instructions', async () => {
163
5
  const { POST } = await import('./upload.js');
164
- mockClose.mockImplementation(() => { throw new Error('not available'); });
165
6
 
166
- const request = createFormDataRequest(createSqliteBuffer());
167
- const response = await POST({ request } as any);
7
+ const response = await POST({ request: new Request('http://localhost/api/db/upload', { method: 'POST' }) } as any);
168
8
  const data = await response.json();
169
9
 
170
- expect(response.status).toBe(200);
171
- expect(data.success).toBe(true);
10
+ expect(response.status).toBe(410);
11
+ expect(data.error).toContain('/api/db/import');
172
12
  });
173
13
  });