Skip to content

feat(snippets): add JSON and ZIP import api with scheduled cleanup jobs - #146

Open
Vicky08100 wants to merge 1 commit into
SudiptaPaul-31:mainfrom
Vicky08100:feat/snippet-import-and-cleanup-jobs
Open

feat(snippets): add JSON and ZIP import api with scheduled cleanup jobs#146
Vicky08100 wants to merge 1 commit into
SudiptaPaul-31:mainfrom
Vicky08100:feat/snippet-import-and-cleanup-jobs

Conversation

@Vicky08100

Copy link
Copy Markdown

Closes #130
Closes #131

PR Description

This PR implements two features that were assigned together: a Snippet Import API that lets users bring in snippets from JSON or ZIP files, and a set of scheduled cleanup jobs that purge expired and stale database records on a daily cron schedule.

What was implemented

Snippet Import API (Issue #130)

I built two new POST endpoints:

  • POST /api/snippets/import/json accepts snippet data either as a raw application/json request body or as a multipart/form-data file upload with a file field. The body can be a single snippet object, an array of snippets, or an object with a snippets array. Every shape is handled and normalised before being passed to the service layer.

  • POST /api/snippets/import/zip accepts a ZIP archive as a multipart/form-data upload. The archive is read entirely in-memory using adm-zip by converting the uploaded file to a Buffer via file.arrayBuffer(). No temporary disk writes happen at any point. For each entry in the ZIP I resolve the programming language from the file extension using a static map (js -> javascript, ts -> typescript, py -> python, etc.). If a source file (e.g. hello.ts) has a companion metadata file with the same base name (e.g. hello.json), that companion file is parsed and its description and tags fields are merged in as the snippet's metadata. macOS __MACOSX junk entries and dot-files are skipped automatically.

Both endpoints authenticate the caller using OwnershipMiddleware.extractWalletAddress(), which tries the x-wallet-address header first and falls back to verifying a Bearer JWT token. If neither is valid the endpoint returns 401. All imported snippets are owned by the authenticated caller's wallet address.

Validation

I added importSnippetSchema to snippet.validator.ts using Zod. It enforces title, code, and language as required non-empty strings, and metadata as a required object (with optional description and tags fields). The optional id field, if present, must be a valid UUID. Validation runs per-snippet in the service layer. Snippets that fail are collected in an errors[] array and returned in the response. Valid snippets continue through to the duplicate check and batch insert. If every snippet in the payload fails validation the endpoint returns HTTP 400.

Duplicate prevention

I implemented a two-stage deduplication strategy:

  1. ID check across all users: checkExistingIds() runs a single WHERE id = ANY($ids) Postgres query and returns a list of already-existing IDs. Any snippet in the import that carries an ID found in this result is skipped and reported as a duplicate.

  2. Content hash check per user: getUserSnippetHashes() fetches the title, language, and Postgres-computed md5(code) for every non-deleted snippet owned by the requesting wallet. I build a Set of title|language|md5_hash composite keys from this result. For each validated snippet I compute the same MD5 hash on the code using Node's crypto.createHash('md5') and construct the same composite key. If it matches an entry in the Set the snippet is skipped and reported as a duplicate. This is an O(1) lookup per snippet after a single DB round-trip, which keeps the import performant even for larger batches.

Batch insert

Snippets that pass validation and both duplicate checks are collected and inserted via createMany() on SnippetRepository. After each successful insert I call appendActivityLog('snippet.created', ...) so the import shows up in the user's activity history.

Scheduled Cleanup Jobs (Issue #131)

I created CleanupService in lib/cleanup.service.ts. It connects to the Neon Postgres database using the same @neondatabase/serverless driver used elsewhere in the project and exposes a single run() method that performs five cleanup steps in sequence:

  1. Deletes rows from snippet_shares where expires_at < NOW() (expired share links).
  2. Deletes rows from auth_sessions where expires_at < NOW() (expired auth sessions).
  3. Deletes rows from login_nonces where expires_at < NOW() (expired login nonces).
  4. Deletes rows from snippets where is_deleted = true AND deleted_at < threshold_date. The threshold defaults to 30 days and is configurable via the CLEANUP_STALE_DELETED_SNIPPETS_DAYS environment variable.
  5. Recursively walks the temporary import directory (defaulting to os.tmpdir()/codely-imports, configurable via CLEANUP_TEMP_DIR) and deletes any file older than 60 minutes (configurable via CLEANUP_TEMP_FILES_MINUTES). Empty subdirectories left behind are also removed.

Each step logs how many records were deleted. The method returns a typed CleanupResult object with counts for each category.

The cleanup is triggered by a Vercel cron job via GET /api/cron/cleanup defined in app/api/cron/cleanup/route.ts. The route is secured by checking that the Authorization header equals Bearer <CRON_SECRET> exactly, which is the pattern Vercel uses when it calls cron endpoints in production. The runtime is explicitly set to nodejs because CleanupService uses Node's fs module. I registered the cron path and schedule in vercel.json so Vercel picks it up automatically:

{
  "path": "/api/cron/cleanup",
  "schedule": "0 2 * * *"
}

## Changes Made

**New files:**
- `app/api/snippets/import/json/route.ts` - POST endpoint for JSON snippet import, handles both raw JSON and multipart file upload
- `app/api/snippets/import/zip/route.ts` - POST endpoint for ZIP archive import, uses adm-zip for in-memory extraction
- `app/api/cron/cleanup/route.ts` - Vercel cron endpoint secured with CRON_SECRET, triggers CleanupService
- `lib/cleanup.service.ts` - CleanupService class with five cleanup jobs and a configurable temp file sweeper
- `lib/cleanup.service.test.ts` - Unit tests for CleanupService covering all five deletion steps

**Modified files:**
- `app/api/snippets/snippet.repository.ts` - Added `checkExistingIds()`, `getUserSnippetHashes()`, and `createMany()` methods
- `app/api/snippets/snippet.service.ts` - Added `importSnippets()` method with per-snippet validation, two-stage deduplication, batch insert, and activity logging; fixed duplicate statements in `deleteSnippet`, `restoreSnippet`, and `permanentlyDeleteSnippet`
- `app/api/snippets/snippet.validator.ts` - Added `importSnippetSchema` and `ImportSnippetDTO` type
- `lib/snippet.service.test.ts` - Added two `importSnippets` test cases; added TextDecoder/TextEncoder polyfills and dummy DATABASE_URL to satisfy the Neon driver in the Jest environment
- `vercel.json` - Registered `/api/cron/cleanup` cron at `0 2 * * *`
- `package.json` / `package-lock.json` - Added `adm-zip` and `@types/adm-zip` as dependencies

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Sudipta 's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 27, 2026

Copy link
Copy Markdown

@Vicky08100 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scheduled Cleanup Jobs Snippet Import API

1 participant