Skip to content
Draft
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
35 changes: 35 additions & 0 deletions .github/workflows/shipment-tracker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Shipment Tracker checks

on:
pull_request:
paths:
- 'applications/shipment-tracker/**'
- '.github/workflows/shipment-tracker.yml'
push:
branches: [main]
paths:
- 'applications/shipment-tracker/**'
- '.github/workflows/shipment-tracker.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-24.04
defaults:
run:
working-directory: applications/shipment-tracker
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.21.0
cache: npm
cache-dependency-path: applications/shipment-tracker/package-lock.json
- run: npm ci --ignore-scripts
- run: npm run typecheck
- run: npm test

# Cloud mutation/CDC acceptance is a separate explicit fixture run; see the README.
12 changes: 12 additions & 0 deletions applications/shipment-tracker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
PGHOST=your-managed-postgres-host
PGPORT=5432
PGDATABASE=postgres
PGUSER=shipments_app
PGPASSWORD=replace-runtime-password
PGSSLROOTCERT=/absolute/path/to/cloud-ca.pem
CLICKHOUSE_URL=https://your-analytical-service:8443
CLICKHOUSE_USER=shipments_reports
CLICKHOUSE_PASSWORD=replace-report-password
ACCOUNT_TOKENS={"00000000-0000-4000-8000-000000000001":"replace-with-a-random-token-at-least-32-characters"}
PORT=3000
# Migration/CDC/admin credentials belong in separate setup files, not this runtime file.
5 changes: 5 additions & 0 deletions applications/shipment-tracker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.deployment/
.env
*.log
1 change: 1 addition & 0 deletions applications/shipment-tracker/.node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.21.0
248 changes: 248 additions & 0 deletions applications/shipment-tracker/README.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions applications/shipment-tracker/infra/clickpipe-events.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"sourceSchemaName": "shipments",
"sourceTable": "events",
"targetTable": "cdc_shipment_events",
"excludedColumns": [],
"sortingKeys": ["account_id", "event_day", "event_id"],
"useCustomSortingKey": true,
"partitionByExpr": "",
"partitionKey": "",
"tableEngine": "ReplacingMergeTree"
}
66 changes: 66 additions & 0 deletions applications/shipment-tracker/migrations/initial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class Initial1700000000000 implements MigrationInterface {
async up(runner: QueryRunner): Promise<void> {
await runner.query(`
CREATE TABLE shipments.accounts (
account_id UUID PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE shipments.shipments (
shipment_id UUID PRIMARY KEY,
account_id UUID NOT NULL REFERENCES shipments.accounts(account_id),
service_level TEXT NOT NULL CHECK (service_level IN ('standard', 'express')),
status TEXT NOT NULL DEFAULT 'created'
CHECK (status IN ('created', 'dispatched', 'delivered', 'cancelled')),
revision INTEGER NOT NULL DEFAULT 0 CHECK (revision BETWEEN 0 AND 10000),
dispatched_at TIMESTAMPTZ(3),
created_at TIMESTAMPTZ(3) NOT NULL,
updated_at TIMESTAMPTZ(3) NOT NULL,
UNIQUE (account_id, shipment_id),
CHECK ((status = 'created' AND dispatched_at IS NULL) OR
(status IN ('dispatched', 'delivered') AND dispatched_at IS NOT NULL) OR
status = 'cancelled')
);
CREATE TABLE shipments.events (
event_id UUID PRIMARY KEY,
account_id UUID NOT NULL,
shipment_id UUID NOT NULL,
request_id TEXT NOT NULL CHECK (length(request_id) BETWEEN 1 AND 64 AND request_id !~ '[^A-Za-z0-9._-]'),
expected_revision INTEGER NOT NULL CHECK (expected_revision BETWEEN 0 AND 9999),
revision INTEGER NOT NULL CHECK (revision = expected_revision + 1),
from_status TEXT NOT NULL,
to_status TEXT NOT NULL,
service_level TEXT NOT NULL CHECK (service_level IN ('standard', 'express')),
event_at TIMESTAMPTZ(3) NOT NULL,
event_day DATE NOT NULL CHECK (event_day = (event_at AT TIME ZONE 'UTC')::DATE),
dispatched_at TIMESTAMPTZ(3),
duration_ms BIGINT NOT NULL DEFAULT 0 CHECK (duration_ms >= 0),
FOREIGN KEY (account_id, shipment_id)
REFERENCES shipments.shipments(account_id, shipment_id),
UNIQUE (account_id, request_id),
UNIQUE (shipment_id, revision),
CHECK ((from_status = 'created' AND to_status IN ('dispatched', 'cancelled')) OR
(from_status = 'dispatched' AND to_status IN ('delivered', 'cancelled'))),
CHECK ((to_status = 'delivered' AND dispatched_at IS NOT NULL AND
event_at >= dispatched_at AND
duration_ms = extract(epoch FROM event_at - dispatched_at) * 1000) OR
(to_status <> 'delivered' AND duration_ms = 0)),
CHECK (to_status <> 'dispatched' OR
(dispatched_at IS NOT NULL AND dispatched_at = event_at)),
CHECK (from_status <> 'dispatched' OR dispatched_at IS NOT NULL)
);
CREATE UNIQUE INDEX events_replica_identity
ON shipments.events(account_id, event_day, event_id);
ALTER TABLE shipments.events REPLICA IDENTITY USING INDEX events_replica_identity;
GRANT SELECT ON shipments.accounts, shipments.shipments, shipments.events TO shipments_app;
GRANT UPDATE (account_id) ON shipments.accounts TO shipments_app;
GRANT UPDATE (status, revision, dispatched_at, updated_at) ON shipments.shipments TO shipments_app;
GRANT INSERT ON shipments.events TO shipments_app;
GRANT SELECT ON shipments.events TO shipments_cdc;
`);
}
async down(runner: QueryRunner): Promise<void> {
await runner.query('DROP TABLE shipments.events, shipments.shipments, shipments.accounts');
}
}
Loading