Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/sdk-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ jobs:
exit 1
fi

- name: Verify GraphQL Codegen
run: |
cd sdk/typescript && npm ci && npm run generate
cd ../python && pip install ariadne-codegen && ariadne-codegen
if [[ -n $(git status --porcelain) ]]; then
echo "::error::Generated SDK types are out of sync with the schema. Run codegen locally and commit the changes."
exit 1
fi

- name: Run tests
run: pip install -e ".[dev]" && pytest -q

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.db import migrations

class Migration(migrations.Migration):

dependencies = [
('ingest', '0053_webhook_replay_job'),
]

operations = [
migrations.RunSQL(
sql="""
-- 1. Rename existing table
ALTER TABLE ingest_contractevent RENAME TO ingest_contractevent_old;

-- 2. Create the new partitioned table (multi-column range)
CREATE TABLE ingest_contractevent (
LIKE ingest_contractevent_old INCLUDING DEFAULTS INCLUDING CONSTRAINTS
) PARTITION BY RANGE ("timestamp", "ledger_sequence");

-- 3. Create the initial active partitions
CREATE TABLE ingest_contractevent_y2026m08 PARTITION OF ingest_contractevent
FOR VALUES FROM ('2026-08-01 00:00:00+00', MINVALUE) TO ('2026-09-01 00:00:00+00', MAXVALUE);

CREATE TABLE ingest_contractevent_y2026m09 PARTITION OF ingest_contractevent
FOR VALUES FROM ('2026-09-01 00:00:00+00', MINVALUE) TO ('2026-10-01 00:00:00+00', MAXVALUE);

-- 4. Migrate data (This will lock the table, schedule during maintenance)
INSERT INTO ingest_contractevent SELECT * FROM ingest_contractevent_old;
""",
reverse_sql="""
DROP TABLE ingest_contractevent;
ALTER TABLE ingest_contractevent_old RENAME TO ingest_contractevent;
"""
)
]
1 change: 1 addition & 0 deletions django-backend/soroscan/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
urlpatterns = [
path("events", views.list_events, name="v1-events"),
path("contracts/<str:contract_id>", views.get_contract, name="v1-contract-detail"),
path("webhooks/dead-letters/bulk-replay", views.bulk_replay_dead_letters, name="v1-webhook-bulk-replay"),
]
47 changes: 47 additions & 0 deletions django-backend/soroscan/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@

from soroscan.ingest.models import ContractEvent, TrackedContract

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from soroscan.ingest.models import WebhookDeadLetter
from soroscan.ingest.services.webhook_replay import create_replay_job


def _isoformat(value) -> str | None:
if value is None:
Expand Down Expand Up @@ -120,3 +127,43 @@ def get_contract(request, contract_id: str):
status=status.HTTP_404_NOT_FOUND,
)
return Response(_contract_to_sdk(contract))


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def bulk_replay_dead_letters(request):
"""
Bulk replay dead-letter webhooks.
Expected payload: {"dead_letter_ids": [1, 2, 3]}
"""
dead_letter_ids = request.data.get("dead_letter_ids", [])
if not isinstance(dead_letter_ids, list) or not dead_letter_ids:
return Response(
{"error": "A non-empty list of dead_letter_ids is required."},
status=status.HTTP_400_BAD_REQUEST
)

dead_letters = WebhookDeadLetter.objects.filter(id__in=dead_letter_ids, resolved=False).select_related('subscription', 'event')

jobs_created = []
for dl in dead_letters:
if dl.event:
# Leverage the existing replay job creation logic
job = create_replay_job(
subscription=dl.subscription,
requested_by=request.user,
contract_id=dl.subscription.contract.contract_id,
event_type=dl.event.event_type,
limit=1
)
jobs_created.append(job.pk)

# Optionally mark as resolved or pending
dl.resolved = True
dl.resolution_note = f"Enqueued replay job {job.pk}"
dl.save(update_fields=["resolved", "resolution_note"])

return Response({
"message": f"Successfully queued {len(jobs_created)} replay jobs.",
"job_ids": jobs_created
}, status=status.HTTP_202_ACCEPTED)
7 changes: 7 additions & 0 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ soroscan = "soroscan.cli:main"

[project.optional-dependencies]
dev = [
"ariadne-codegen>=0.9.0",
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"pytest-asyncio>=0.23.0",
Expand All @@ -43,6 +44,12 @@ dev = [
"pact-python>=3.4.0",
]

[tool.ariadne-codegen]
schema = "../../django-backend/schema.graphql"
target_package = "soroscan.generated"
target_directory = "soroscan/"
type_hints = "pydantic"

[project.urls]
Homepage = "https://soroscan.io"
Documentation = "https://docs.soroscan.io"
Expand Down
11 changes: 11 additions & 0 deletions sdk/typescript/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
schema: '../../django-backend/schema.graphql', // Path to backend schema
generates: {
'./src/generated/graphql.ts': {
plugins: ['typescript']
}
}
};
export default config;
5 changes: 4 additions & 1 deletion sdk/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"LICENSE"
],
"scripts": {
"build": "npm run build:esm && npm run build:cjs && npm run build:cjs:rename",
"generate": "graphql-codegen --config codegen.ts",
"build": "npm run generate && npm run build:esm && npm run build:cjs && npm run build:cjs:rename",
"build:esm": "tsc -p tsconfig.json --outDir dist/esm --module esnext --moduleResolution bundler --declaration --declarationDir dist/esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:cjs:rename": "node scripts/rename-cjs.mjs",
Expand All @@ -52,6 +53,8 @@
"prepublishOnly": "npm run build && npm test"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.0",
"@pact-foundation/pact": "^13.2.0",
"@types/node": "^22.0.0",
"typescript": "^5.4.5",
Expand Down
Loading