feat(snippets): add JSON and ZIP import api with scheduled cleanup jobs - #146
Open
Vicky08100 wants to merge 1 commit into
Open
feat(snippets): add JSON and ZIP import api with scheduled cleanup jobs#146Vicky08100 wants to merge 1 commit into
Vicky08100 wants to merge 1 commit into
Conversation
|
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. |
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/jsonaccepts snippet data either as a rawapplication/jsonrequest body or as amultipart/form-datafile upload with afilefield. The body can be a single snippet object, an array of snippets, or an object with asnippetsarray. Every shape is handled and normalised before being passed to the service layer.POST /api/snippets/import/zipaccepts a ZIP archive as amultipart/form-dataupload. The archive is read entirely in-memory usingadm-zipby converting the uploaded file to aBufferviafile.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 itsdescriptionandtagsfields are merged in as the snippet's metadata. macOS__MACOSXjunk entries and dot-files are skipped automatically.Both endpoints authenticate the caller using
OwnershipMiddleware.extractWalletAddress(), which tries thex-wallet-addressheader 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
importSnippetSchematosnippet.validator.tsusing Zod. It enforcestitle,code, andlanguageas required non-empty strings, andmetadataas a required object (with optionaldescriptionandtagsfields). The optionalidfield, if present, must be a valid UUID. Validation runs per-snippet in the service layer. Snippets that fail are collected in anerrors[]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:
ID check across all users:
checkExistingIds()runs a singleWHERE 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.Content hash check per user:
getUserSnippetHashes()fetches thetitle,language, and Postgres-computedmd5(code)for every non-deleted snippet owned by the requesting wallet. I build aSetoftitle|language|md5_hashcomposite keys from this result. For each validated snippet I compute the same MD5 hash on the code using Node'scrypto.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()onSnippetRepository. After each successful insert I callappendActivityLog('snippet.created', ...)so the import shows up in the user's activity history.Scheduled Cleanup Jobs (Issue #131)
I created
CleanupServiceinlib/cleanup.service.ts. It connects to the Neon Postgres database using the same@neondatabase/serverlessdriver used elsewhere in the project and exposes a singlerun()method that performs five cleanup steps in sequence:snippet_shareswhereexpires_at < NOW()(expired share links).auth_sessionswhereexpires_at < NOW()(expired auth sessions).login_nonceswhereexpires_at < NOW()(expired login nonces).snippetswhereis_deleted = true AND deleted_at < threshold_date. The threshold defaults to 30 days and is configurable via theCLEANUP_STALE_DELETED_SNIPPETS_DAYSenvironment variable.os.tmpdir()/codely-imports, configurable viaCLEANUP_TEMP_DIR) and deletes any file older than 60 minutes (configurable viaCLEANUP_TEMP_FILES_MINUTES). Empty subdirectories left behind are also removed.Each step logs how many records were deleted. The method returns a typed
CleanupResultobject with counts for each category.The cleanup is triggered by a Vercel cron job via
GET /api/cron/cleanupdefined inapp/api/cron/cleanup/route.ts. The route is secured by checking that theAuthorizationheader equalsBearer <CRON_SECRET>exactly, which is the pattern Vercel uses when it calls cron endpoints in production. The runtime is explicitly set tonodejsbecauseCleanupServiceuses Node'sfsmodule. I registered the cron path and schedule invercel.jsonso 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