diff --git a/.dev.example.vars b/.dev.example.vars new file mode 100644 index 0000000..8d85a07 --- /dev/null +++ b/.dev.example.vars @@ -0,0 +1,6 @@ +DATABASE_URL="postgresql://comuline:password@localhost:5432/comuline" +COMULINE_ENV="development" + +# Take token from .env.db +UPSTASH_REDIS_REST_TOKEN="" +UPSTASH_REDIS_REST_URL="http://localhost:8079" \ No newline at end of file diff --git a/.env.db b/.env.db index 3977482..46a866b 100644 --- a/.env.db +++ b/.env.db @@ -1,3 +1,10 @@ +# PostgreSQL POSTGRES_USER="comuline" POSTGRES_PASSWORD="password" -POSTGRES_DB="comuline" \ No newline at end of file +POSTGRES_DB="comuline" + +# Redis +SRH_MODE=env +# openssl rand -base64 32 +SRH_TOKEN="1Pf91ZNy5LDTKG621uhX/E73P8RmhVZu43kIV/WCmHg=" +SRH_CONNECTION_STRING=redis://redis:6379 \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index b517e92..0000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -REDIS_URL="redis://localhost:6379" -DATABASE_URL="postgresql://comuline:password@localhost:5432/comuline" -# SYNC_TOKEN is a secret key used in production level to authenticate requests to the POST /v1/station and POST /v1/schedule endpoint. -# You can generate a new secret on the command line with: -# openssl rand -base64 32 -SYNC_TOKEN="" diff --git a/.github/workflows/api.build.yml b/.github/workflows/api.build.yml new file mode 100644 index 0000000..6d43e30 --- /dev/null +++ b/.github/workflows/api.build.yml @@ -0,0 +1,28 @@ +name: Build API on Pull Request + +on: + pull_request: + branches: ["main"] + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "latest" + + - name: Install package deps + run: bun i + + - name: Build package + run: bun run build diff --git a/.github/workflows/api.deploy.yml b/.github/workflows/api.deploy.yml new file mode 100644 index 0000000..a36a489 --- /dev/null +++ b/.github/workflows/api.deploy.yml @@ -0,0 +1,28 @@ +name: Deploy API to Cloudflare Workers + +on: + push: + branches: ["main"] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Installing Bun + uses: oven-sh/setup-bun@v2 + + - name: Install package deps + run: bun i + + - name: Build package + run: bun run build + + - name: Deploy to Cloudflare Workers + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy --minify src/index.ts diff --git a/.gitignore b/.gitignore index 5a4337e..e9a89b5 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,9 @@ yarn-error.log* package-lock.json **/*.bun .env +wrangler.toml +.wrangler +.dev.vars + +# build +.dist \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0de5967..0000000 --- a/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM oven/bun - -WORKDIR /app - -COPY package.json . -COPY bun.lockb . - -# TODO: RUN MIGRATION ON PRODUCTION, HOW TF DO I DO THAT? -RUN bun install - -COPY src src -COPY tsconfig.json . - -ENV NODE_ENV production - -CMD ["bun", "src/index.ts"] - -EXPOSE 3000 \ No newline at end of file diff --git a/README.md b/README.md index 8dff5db..679579e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @comuline/api -An API to get the schedule of KRL commuter line in Jakarta and Yogyakarta using [Elsyia](https://elysiajs.com/) and [Bun](https://bun.sh/), deployed to [Render](https://render.com/). This API is primarily used on the [web app](https://comuline.com/) ([source code](https://github.com/comuline/web)). +An API to get the schedule of KRL commuter line in Jakarta and Yogyakarta using [Hono](https://hono.dev/) and [Bun](https://bun.sh/), deployed to [Cloudflare Workers](https://workers.cloudflare.com/). This API is primarily used on the [web app](https://comuline.com/) ([source code](https://github.com/comuline/web)). ### How does it work? @@ -8,11 +8,11 @@ This API uses a daily cron job (at 00:00) to fetch the schedule of KRL commuter ### Technology stacks -1. [Elsyia](https://elysiajs.com/) API framework +1. [Hono](https://hono.dev/) API framework 2. [Bun](https://bun.sh/) runtime -3. PostgresSQL ([Neon](https://neon.tech/)) -4. Redis ([Upstash](https://upstash.com/)) -5. [Render](https://render.com/) deployment platform +3. (Serverless) PostgresSQL ([Neon](https://neon.tech/)) +4. (Serverless) Redis ([Upstash](https://upstash.com/)) +5. [Cloudflare Workers](https://workers.cloudflare.com/) deployment platform 6. [Drizzle](https://orm.drizzle.team/) ORM ## Getting Started @@ -31,42 +31,44 @@ git clone https://github.com/comuline/api.git bun install ``` -3. Run database locally +3. Copy the `.dev.example.vars` to `.dev.vars` -```bash -docker-compose up -d +``` +cp .dev.example.vars .dev.vars ``` -4. Copy the `.env.example` to `.env` +4. Generate `UPSTASH_REDIS_REST_TOKEN` using `openssl rand -hex 32` and copy it to your `.dev.vars` file -``` -cp .env.example .env +5. Run database locally + +```bash +docker-compose up -d ``` -5. Run the database migration +6. Run the database migration ```bash -bun db:generate && bun db:migrate +bun run migrate:apply ``` -6. Sync the data and populate it into your local database (once only as you needed) +7. Sync the data and populate it into your local database (once only as you needed) ```bash # Please do this in order # 1. Sync station data and wait until it's done -curl --request POST --url http://localhost:3001/v1/station/ +bun run sync:station # 2. Sync schedule data -curl --request POST --url http://localhost:3001/v1/schedule/ +bun run sync:schedule ``` ### Deployment -1. Create a new PostgreSQL database in [Neon](https://neon.tech/) and copy the connection string value as `DATABASE_URL` in your `.env` file +1. Create a new PostgreSQL database in [Neon](https://neon.tech/) and copy the connection string value as `DATABASE_URL` in your `.production.vars` file 2. Run the database migration ```bash -bun db:generate && bun db:migrate +bun run migrate:apply ``` 3. Sync the data and populate it into your remote database (once only as you needed) @@ -74,70 +76,31 @@ bun db:generate && bun db:migrate ```bash # Please do this in order # 1. Sync station data and wait until it's done -curl --request POST --url http://localhost:3001/v1/station/ +bun run sync:station # 2. Sync schedule data -curl --request POST --url http://localhost:3001/v1/schedule/ - +bun run sync:schedule ``` -4. Generate `SYNC_TOKEN` (This is used in production level only to prevent unauthorized access to your `POST /v1/station` and `POST /v1/schedule` endpoint) +4. Add `COMULINE_ENV` to your `.production.vars` file -```bash -openssl rand -base64 32 -# Copy the output value as a `SYNC_TOKEN` +``` +COMULINE_ENV=production ``` -2. Create a new Redis database in [Upstash](https://upstash.com/) and copy the connection string value as `REDIS_URL` +5. Create a new Redis database in [Upstash](https://upstash.com/) and copy the value of `UPSTASH_REDIS_REST_TOKEN` and `UPSTASH_REDIS_REST_URL` to your `.production.vars` file -3. Create a `Web Service` in [Render](https://render.com/), copy the `DATABASE_URL`, `REDIS_URL`, and `SYNC_TOKEN` as environment variables, and deploy the application. +6. Save your `.production.vars` file to your environment variables in your Cloudflare Workers using `wrangler` + +```bash +bunx wrangler secret put --env production $(cat .production.vars) +``` -4. Set the cron job to fetch the schedule data using [Cron-Job](https://cron-job.org/en/). Don't forget to set the `SYNC_TOKEN` as a header in your request. Add the `?from_cron=true` query parameter to flag the request as a cron job request. +6. Deploy the API to Cloudflare Workers ```bash -# Example -curl --request POST --url https://your-service-name.onrender.com/v1/station?from_cron=true -H "Authorization: Bearer ${SYNC_TOKEN}" -curl --request POST --url https://your-service-name.onrender.com/v1/schedule?from_cron=true -H "Authorization: Bearer ${SYNC_TOKEN}" +bun run deploy ``` ### Database schema -> **Station** - -| Column Name | Data Type | Description | -| ------------ | --------- | ------------------------------- | -| id | TEXT | Primary key (Station ID) | -| name | TEXT | Station name | -| daop | INTEGER | Station regional operation code | -| fgEnable | BOOLEAN | - | -| haveSchedule | BOOLEAN | Schedule availability status | -| updatedAt | TEXT | Last updated date | - -> **Schedule** - -| Column Name | Data Type | Description | -| --------------- | --------- | ----------------------------------- | -| id | TEXT | Primary key (Station ID + Train ID) | -| stationId | TEXT | Station ID | -| trainId | TEXT | Train ID | -| line | TEXT | Train commuter line | -| route | TEXT | Train route | -| color | TEXT | Commuter line color | -| destination | TEXT | Train destination | -| timeEstimated | TIME | Estimated time | -| destinationTime | TIME | Destination time | -| updatedAt | TEXT | Last updated date | - -> **Sync** - -| Column Name | Data Type | Description | -| ----------- | --------- | -------------------------------------- | -| id | TEXT | Primary key (Sync ID) | -| n | BIGINT | n of sync | -| type | ENUM | Sync type (manual, cron) | -| status | ENUM | Sync status (PENDING, SUCCESS, FAILED) | -| item | ENUM | Sync item (station, schedule) | -| duration | BIGINT | Sync duration | -| message | TEXT | Sync message (if status failed) | -| startedAt | TEXT | Sync started date | -| endedAt | TEXT | Sync ended date | -| createdAt | TEXT | Sync created date | +> TBD diff --git a/bun.lockb b/bun.lockb index 4466669..7982f2f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml index c54e5e2..3971fd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,34 @@ version: "3.9" services: + # Serverless PostgresSQL postgres: image: "postgres:15.2-alpine" - restart: always - container_name: "comuline-db" ports: - "5432:5432" - volumes: - - db:/var/lib/postgresql/data env_file: - ./.env.db + pg_proxy: + image: ghcr.io/neondatabase/wsproxy:latest + environment: + APPEND_PORT: "postgres:5432" + ALLOW_ADDR_REGEX: ".*" + LOG_TRAFFIC: "true" + ports: + - "5433:80" + depends_on: + - postgres + + # Serverless Redis redis: image: redis - container_name: "comuline-cache" ports: - "6379:6379" - -volumes: - db: + serverless-redis-http: + ports: + - "8079:80" + image: hiett/serverless-redis-http:latest + env_file: + - ./.env.db + depends_on: + - redis diff --git a/drizzle.config.ts b/drizzle.config.ts index b21da4a..6651542 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,7 @@ import type { Config } from "drizzle-kit" export default { + out: "./drizzle/migrations", + dialect: "postgresql", schema: "./src/db/schema", - out: "./src/db/migrations", - driver: "pg", - dbCredentials: { - connectionString: process.env.DATABASE_URL!, - }, } satisfies Config diff --git a/drizzle/migrations/0000_talented_daimon_hellstrom.sql b/drizzle/migrations/0000_talented_daimon_hellstrom.sql new file mode 100644 index 0000000..07f8163 --- /dev/null +++ b/drizzle/migrations/0000_talented_daimon_hellstrom.sql @@ -0,0 +1,57 @@ +DO $$ BEGIN + CREATE TYPE "public"."station_type" AS ENUM('KRL', 'MRT', 'LRT'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "schedule" ( + "id" text PRIMARY KEY NOT NULL, + "station_id" text NOT NULL, + "station_origin_id" text, + "station_destination_id" text, + "train_id" text NOT NULL, + "line" text NOT NULL, + "route" text NOT NULL, + "time_departure" time NOT NULL, + "time_at_destination" time NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "schedule_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "station" ( + "uid" text PRIMARY KEY NOT NULL, + "id" text NOT NULL, + "name" text NOT NULL, + "type" "station_type" NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "station_uid_unique" UNIQUE("uid"), + CONSTRAINT "station_id_unique" UNIQUE("id") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_id_station_id_fk" FOREIGN KEY ("station_id") REFERENCES "public"."station"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_origin_id_station_id_fk" FOREIGN KEY ("station_origin_id") REFERENCES "public"."station"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_destination_id_station_id_fk" FOREIGN KEY ("station_destination_id") REFERENCES "public"."station"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "schedule_idx" ON "schedule" USING btree ("id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "schedule_station_idx" ON "schedule" USING btree ("station_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "station_uidx" ON "station" USING btree ("uid");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_idx" ON "station" USING btree ("id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "station_type_idx" ON "station" USING btree ("type"); \ No newline at end of file diff --git a/drizzle/migrations/0001_tiny_shadowcat.sql b/drizzle/migrations/0001_tiny_shadowcat.sql new file mode 100644 index 0000000..7ffbb3a --- /dev/null +++ b/drizzle/migrations/0001_tiny_shadowcat.sql @@ -0,0 +1 @@ +ALTER TYPE "station_type" ADD VALUE 'LOCAL'; \ No newline at end of file diff --git a/drizzle/migrations/0002_serious_the_hand.sql b/drizzle/migrations/0002_serious_the_hand.sql new file mode 100644 index 0000000..a4dfe01 --- /dev/null +++ b/drizzle/migrations/0002_serious_the_hand.sql @@ -0,0 +1,23 @@ +ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_id_station_id_fk"; +--> statement-breakpoint +ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_origin_id_station_id_fk"; +--> statement-breakpoint +ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_destination_id_station_id_fk"; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_id_station_id_fk" FOREIGN KEY ("station_id") REFERENCES "public"."station"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_origin_id_station_id_fk" FOREIGN KEY ("station_origin_id") REFERENCES "public"."station"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_destination_id_station_id_fk" FOREIGN KEY ("station_destination_id") REFERENCES "public"."station"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/migrations/0003_first_dorian_gray.sql b/drizzle/migrations/0003_first_dorian_gray.sql new file mode 100644 index 0000000..dea4939 --- /dev/null +++ b/drizzle/migrations/0003_first_dorian_gray.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS "schedule_train_idx" ON "schedule" USING btree ("train_id"); \ No newline at end of file diff --git a/drizzle/migrations/0004_great_vance_astro.sql b/drizzle/migrations/0004_great_vance_astro.sql new file mode 100644 index 0000000..de9cecc --- /dev/null +++ b/drizzle/migrations/0004_great_vance_astro.sql @@ -0,0 +1,4 @@ +ALTER TABLE "schedule" ADD COLUMN "departs_at" timestamp with time zone DEFAULT now();--> statement-breakpoint +ALTER TABLE "schedule" ADD COLUMN "arrives_at" timestamp with time zone DEFAULT now();--> statement-breakpoint +ALTER TABLE "schedule" DROP COLUMN IF EXISTS "time_departure";--> statement-breakpoint +ALTER TABLE "schedule" DROP COLUMN IF EXISTS "time_at_destination"; \ No newline at end of file diff --git a/drizzle/migrations/0005_rare_senator_kelly.sql b/drizzle/migrations/0005_rare_senator_kelly.sql new file mode 100644 index 0000000..3aefcdb --- /dev/null +++ b/drizzle/migrations/0005_rare_senator_kelly.sql @@ -0,0 +1,4 @@ +ALTER TABLE "schedule" ALTER COLUMN "departs_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "arrives_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "updated_at" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/migrations/0006_hesitant_hedge_knight.sql b/drizzle/migrations/0006_hesitant_hedge_knight.sql new file mode 100644 index 0000000..aae3167 --- /dev/null +++ b/drizzle/migrations/0006_hesitant_hedge_knight.sql @@ -0,0 +1,2 @@ +ALTER TABLE "station" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "station" ALTER COLUMN "updated_at" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/migrations/0007_naive_pepper_potts.sql b/drizzle/migrations/0007_naive_pepper_potts.sql new file mode 100644 index 0000000..818eb18 --- /dev/null +++ b/drizzle/migrations/0007_naive_pepper_potts.sql @@ -0,0 +1,2 @@ +ALTER TABLE "schedule" ALTER COLUMN "station_origin_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "schedule" ALTER COLUMN "station_destination_id" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..3f6a54f --- /dev/null +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -0,0 +1,305 @@ +{ + "id": "ebac93b2-10b0-46c7-8348-9827ee12aef5", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3a6e3f7 --- /dev/null +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,306 @@ +{ + "id": "dd1edb26-6f6a-4a0b-856b-8b73207f9055", + "prevId": "ebac93b2-10b0-46c7-8348-9827ee12aef5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0002_snapshot.json b/drizzle/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..a919664 --- /dev/null +++ b/drizzle/migrations/meta/0002_snapshot.json @@ -0,0 +1,306 @@ +{ + "id": "5db983ea-4af8-40b5-9bca-1f274a3c4e3a", + "prevId": "dd1edb26-6f6a-4a0b-856b-8b73207f9055", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0003_snapshot.json b/drizzle/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..a020147 --- /dev/null +++ b/drizzle/migrations/meta/0003_snapshot.json @@ -0,0 +1,321 @@ +{ + "id": "d2d9cd87-d69d-46ff-9890-21be36c0cb3b", + "prevId": "5db983ea-4af8-40b5-9bca-1f274a3c4e3a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_departure": { + "name": "time_departure", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "time_at_destination": { + "name": "time_at_destination", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0004_snapshot.json b/drizzle/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..4c99c04 --- /dev/null +++ b/drizzle/migrations/meta/0004_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "f1783790-c69c-479f-a903-cc6365ccfed6", + "prevId": "d2d9cd87-d69d-46ff-9890-21be36c0cb3b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0005_snapshot.json b/drizzle/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..e190837 --- /dev/null +++ b/drizzle/migrations/meta/0005_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "65f42182-0d09-4f8a-b7f5-f593648f5c31", + "prevId": "f1783790-c69c-479f-a903-cc6365ccfed6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0006_snapshot.json b/drizzle/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..70d122e --- /dev/null +++ b/drizzle/migrations/meta/0006_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "bab82009-af8d-4269-8815-e14cb1f6302d", + "prevId": "65f42182-0d09-4f8a-b7f5-f593648f5c31", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/0007_snapshot.json b/drizzle/migrations/meta/0007_snapshot.json new file mode 100644 index 0000000..beb332d --- /dev/null +++ b/drizzle/migrations/meta/0007_snapshot.json @@ -0,0 +1,323 @@ +{ + "id": "511ffc79-573f-46fb-adaf-1de89c7f7d69", + "prevId": "bab82009-af8d-4269-8815-e14cb1f6302d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.station": { + "name": "station", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "station_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "station_uidx": { + "name": "station_uidx", + "columns": [ + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_idx": { + "name": "station_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "station_type_idx": { + "name": "station_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "station_uid_unique": { + "name": "station_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + }, + "station_id_unique": { + "name": "station_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "station_id": { + "name": "station_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_origin_id": { + "name": "station_origin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "station_destination_id": { + "name": "station_destination_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "train_id": { + "name": "train_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departs_at": { + "name": "departs_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "arrives_at": { + "name": "arrives_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schedule_idx": { + "name": "schedule_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_station_idx": { + "name": "schedule_station_idx", + "columns": [ + { + "expression": "station_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedule_train_idx": { + "name": "schedule_train_idx", + "columns": [ + { + "expression": "train_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_station_id_station_id_fk": { + "name": "schedule_station_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_station_origin_id_station_id_fk": { + "name": "schedule_station_origin_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_station_destination_id_station_id_fk": { + "name": "schedule_station_destination_id_station_id_fk", + "tableFrom": "schedule", + "tableTo": "station", + "columnsFrom": [ + "station_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schedule_id_unique": { + "name": "schedule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": { + "public.station_type": { + "name": "station_type", + "schema": "public", + "values": [ + "KRL", + "MRT", + "LRT", + "LOCAL" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json new file mode 100644 index 0000000..7b19e5b --- /dev/null +++ b/drizzle/migrations/meta/_journal.json @@ -0,0 +1,62 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1731395911889, + "tag": "0000_talented_daimon_hellstrom", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1731396377710, + "tag": "0001_tiny_shadowcat", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1731399355344, + "tag": "0002_serious_the_hand", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1731404897712, + "tag": "0003_first_dorian_gray", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1731486602497, + "tag": "0004_great_vance_astro", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1731487109892, + "tag": "0005_rare_senator_kelly", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1731489275577, + "tag": "0006_hesitant_hedge_knight", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1732445107060, + "tag": "0007_naive_pepper_potts", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index f934400..d95922b 100644 --- a/package.json +++ b/package.json @@ -5,39 +5,45 @@ "url": "https://github.com/comuline", "email": "support@comuline.com" }, - "version": "1.0.50", + "version": "2.0", "scripts": { + "dev": "wrangler dev src/index.ts --port 3001", + "build": "tsup --clean", + "docker:up": "docker-compose -p comuline-api up -d", + "docker:down": "docker-compose down", "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --watch src/index.ts", - "start": "bun run src/index.ts", - "db:push": "drizzle-kit push:pg", - "db:studio": "drizzle-kit studio", - "db:generate": "drizzle-kit generate:pg", - "db:migrate": "bun run src/db/migrate.ts", - "db:pull": "drizzle-kit introspect:pg", - "db:check": "drizzle-kit check:pg", + "deploy": "wrangler deploy --minify src/index.ts", + "migrate:drop": "drizzle-kit drop", + "migrate:generate": "drizzle-kit generate", + "migrate:apply": "bun run src/db/migrate.ts", "format": "prettier -w .", "format:check": "prettier -c .", - "prepare": "husky" + "prepare": "husky", + "sync:schedule": "bun run --env-file .dev.vars src/sync/schedule.ts", + "sync:station": "bun run --env-file .dev.vars src/sync/station.ts" }, "dependencies": { - "@elysiajs/swagger": "^0.8.5", - "drizzle-orm": "^0.30.1", - "elysia": "latest", - "elysia-rate-limit": "^2.1.0", - "ioredis": "^5.3.2", - "pg": "^8.11.3", - "pino": "^8.19.0", - "pino-pretty": "^10.3.1", - "postgres": "^3.4.3", - "zod": "^3.22.4" + "@hono/zod-openapi": "^0.16.0", + "@neondatabase/serverless": "^0.9.5", + "@scalar/hono-api-reference": "^0.5.145", + "@upstash/redis": "^1.34.3", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.33.0", + "drizzle-zod": "^0.5.1", + "hono": "^4.5.11", + "postgres": "^3.4.4", + "zod": "^3.23.8" }, "devDependencies": { + "@cloudflare/workers-types": "^4.20240903.0", "bun-types": "latest", - "drizzle-kit": "^0.20.14", + "drizzle-kit": "^0.24.2", "husky": "^9.0.11", "lint-staged": "^15.2.2", - "prettier": "^3.2.5" + "prettier": "^3.2.5", + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "wrangler": "^3.86.1" }, "module": "src/index.js", "lint-staged": { diff --git a/src/commons/libs/cache.ts b/src/commons/libs/cache.ts deleted file mode 100644 index 4aba9af..0000000 --- a/src/commons/libs/cache.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Redis from "ioredis" - -if (!process.env.REDIS_URL) throw new Error("REDIS_URL is not set") - -const cache = new Redis(process.env.REDIS_URL) - -export default cache diff --git a/src/commons/libs/db.ts b/src/commons/libs/db.ts deleted file mode 100644 index 35b5891..0000000 --- a/src/commons/libs/db.ts +++ /dev/null @@ -1,8 +0,0 @@ -import postgres from "postgres" - -if (!process.env.DATABASE_URL) - throw new Error("Cannot migrate. DATABASE_URL is not set") - -export const db = postgres(process.env.DATABASE_URL) - -export default db diff --git a/src/commons/libs/swagger.ts b/src/commons/libs/swagger.ts deleted file mode 100644 index a409160..0000000 --- a/src/commons/libs/swagger.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { swagger as primitiveSwagger } from "@elysiajs/swagger" - -const swagger = () => - primitiveSwagger({ - path: "/docs", - exclude: ["/docs", "/docs/json", "/", "/health"], - documentation: { - info: { - title: "Comuline API", - description: "API documentation for Comuline API", - version: "1.0.0", - }, - tags: [ - { - name: "Station", - description: "Station related endpoints", - }, - { - name: "Schedule", - description: "Schedule related endpoints", - }, - { - name: "Route", - description: "Route related endpoints", - }, - { - name: "Utility", - description: "Utility related endpoints", - }, - ], - }, - }) - -export default swagger diff --git a/src/commons/types.ts b/src/commons/types.ts deleted file mode 100644 index b41da1b..0000000 --- a/src/commons/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { t } from "elysia" - -export type APIResponse = { - data: T - status: number - message: "OK" | "ERROR" | "NOT_FOUND" | string -} - -export const syncResponse = (item: SyncItem) => ({ - 200: t.Object( - { - status: t.Number(), - data: t.Object({ - id: t.String(), - status: t.String(), - type: t.Union([t.Literal("manual"), t.Literal("cron")]), - item: t.Union([t.Literal("station"), t.Literal("schedule")]), - }), - }, - { - default: { - status: 200, - data: { - id: "08dd3ed8-8dd7-4c0d-8463-7422ce3e07b9", - type: "manual", - item, - status: "PENDING", - }, - }, - }, - ), -}) - -export const scheduleResponseObject = { - id: t.Nullable(t.String()), - name: t.Nullable(t.String()), - daop: t.Nullable(t.Number()), - fgEnable: t.Nullable(t.Number()), - haveSchedule: t.Nullable(t.Boolean()), - updatedAt: t.Nullable(t.String()), -} - -export const syncResponseObject = { - id: t.Nullable(t.String()), - n: t.Number(), - type: t.Nullable(t.String()), - status: t.Nullable(t.String()), - item: t.Nullable(t.String()), - duration: t.Nullable(t.Number()), - message: t.Nullable(t.String()), - startedAt: t.Nullable(t.String()), - endedAt: t.Nullable(t.String()), - createdAt: t.Nullable(t.String()), -} - -export type SyncType = "manual" | "cron" - -export type SyncItem = "station" | "schedule" diff --git a/src/commons/utils/cache.ts b/src/commons/utils/cache.ts deleted file mode 100644 index c887043..0000000 --- a/src/commons/utils/cache.ts +++ /dev/null @@ -1,35 +0,0 @@ -import cache from "../libs/cache" - -class Cache { - protected ttl: number | null - public key: string - public cached: T | null - - constructor(key: string, options?: { ttl?: number }) { - this.ttl = options ? options.ttl ?? null : null - this.key = key - this.cached = null - } - - async set(value: T) { - const self = this - await cache.set( - self.key, - typeof value === "string" ? value : JSON.stringify(value), - ) - if (self.ttl) return await cache.expire(self.key, self.ttl) - - return - } - - async get() { - const self = this - const data = await cache.get(self.key) - if (data) { - self.cached = JSON.parse(data) as T - } - return self.cached - } -} - -export default Cache diff --git a/src/commons/utils/date.ts b/src/commons/utils/date.ts deleted file mode 100644 index 3f35938..0000000 --- a/src/commons/utils/date.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function parseTime(timeString: string): Date { - const [hours, minutes, seconds] = timeString.split(":").map(Number) - const date = new Date() - date.setHours(hours ?? date.getHours()) - date.setMinutes(minutes ?? date.getMinutes()) - date.setSeconds(seconds ?? date.getSeconds()) - - return date -} - -export function getSecondsRemainingFromNow(): number { - return ( - 60 * new Date(Date.now()).getMinutes() * new Date(Date.now()).getHours() - ) -} diff --git a/src/commons/utils/error.ts b/src/commons/utils/error.ts deleted file mode 100644 index e45998f..0000000 --- a/src/commons/utils/error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function handleError(e: any): string { - if (e instanceof Error) { - return e.message - } - return JSON.stringify(e) -} diff --git a/src/commons/utils/log.ts b/src/commons/utils/log.ts deleted file mode 100644 index ee92e8a..0000000 --- a/src/commons/utils/log.ts +++ /dev/null @@ -1,21 +0,0 @@ -import pino from "pino" - -const transport = pino.transport({ - targets: [ - // Uncomment the following lines to log to a file in your local machine - /* { - level: "trace", - target: "pino/file", - options: { - destination: "./logs/file.log", - }, - }, */ - { - level: "trace", - target: "pino-pretty", - options: {}, - }, - ], -}) - -export const logger = pino({}, transport) diff --git a/src/controllers/index.ts b/src/controllers/index.ts deleted file mode 100644 index ac545e8..0000000 --- a/src/controllers/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Elysia, { NotFoundError } from "elysia" -import { APIResponse } from "../commons/types" -import scheduleController from "./schedule" -import stationController from "./station" -import syncController from "./sync" -import routeController from "./route" - -const controllers = new Elysia({ prefix: "/v1" }) - .onError((ctx) => { - return { - status: (ctx.error as NotFoundError).status ?? 500, - message: ctx.error.message.includes("{") - ? JSON.parse(ctx.error.message) - : ctx.error.message, - } satisfies Partial - }) - .use(stationController) - .use(scheduleController) - .use(routeController) - .use(syncController) - -export default controllers diff --git a/src/controllers/route.ts b/src/controllers/route.ts deleted file mode 100644 index 588a58e..0000000 --- a/src/controllers/route.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Elysia, InternalServerError, t } from "elysia" -import * as service from "../services" -import { SyncType, syncResponse } from "../commons/types" - -const routeController = (app: Elysia) => - app.group("/route", (app) => { - app.get( - "/:trainId", - async (ctx) => { - if (ctx.query.from_station_id) - return await service.route.getAllFrom( - ctx.params.trainId, - ctx.query.from_station_id.toLocaleUpperCase(), - ) - return await service.route.getAll(ctx.params.trainId) - }, - { - params: t.Object({ - trainId: t.String(), - }), - query: t.Object({ - from_station_id: t.Optional(t.String()), - }), - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Route data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Array( - t.Object({ - id: t.Nullable(t.String()), - stationId: t.Nullable(t.String()), - stationName: t.Nullable(t.String()), - trainId: t.Nullable(t.String()), - line: t.Nullable(t.String()), - route: t.Nullable(t.String()), - color: t.Nullable(t.String()), - destination: t.Nullable(t.String()), - timeEstimated: t.Nullable(t.String()), - destinationTime: t.Nullable(t.String()), - updatedAt: t.Nullable(t.String()), - }), - ), - }, - { - default: { - status: 200, - data: [ - { - id: "BKS-5171", - stationId: "BKS", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:47:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:08.132Z", - stationName: "BEKASI", - }, - { - id: "KRI-5171", - stationId: "KRI", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:50:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:14.794Z", - stationName: "KRANJI", - }, - { - id: "CUK-5171", - stationId: "CUK", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:56:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:10.959Z", - stationName: "CAKUNG", - }, - { - id: "KLDB-5171", - stationId: "KLDB", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:58:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:14.141Z", - stationName: "KLENDERBARU", - }, - { - id: "BUA-5171", - stationId: "BUA", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "21:59:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:08.931Z", - stationName: "BUARAN", - }, - { - id: "KLD-5171", - stationId: "KLD", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:01:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:14.080Z", - stationName: "KLENDER", - }, - { - id: "JNG-5171", - stationId: "JNG", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:10:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:13.396Z", - stationName: "JATINEGARA", - }, - { - id: "MTR-5171", - stationId: "MTR", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:12:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:10.858Z", - stationName: "MATRAMAN", - }, - { - id: "MRI-5171", - stationId: "MRI", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:18:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:15.636Z", - stationName: "MANGGARAI", - }, - { - id: "SUD-5171", - stationId: "SUD", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:22:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:18.281Z", - stationName: "SUDIRMAN", - }, - { - id: "SUDB-5171", - stationId: "SUDB", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:23:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:11.019Z", - stationName: "SUDIRMAN BARU", - }, - { - id: "KAT-5171", - stationId: "KAT", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:24:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:13.317Z", - stationName: "KARET", - }, - { - id: "THB-5171", - stationId: "THB", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:30:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:18.884Z", - stationName: "TANAHABANG", - }, - { - id: "DU-5171", - stationId: "DU", - trainId: "5171", - line: "COMMUTER LINE CIKARANG", - route: "BEKASI-ANGKE", - color: "#0084D8", - destination: "ANGKE", - timeEstimated: "22:38:00", - destinationTime: "22:41:00", - updatedAt: "2024-03-16T17:00:12.541Z", - stationName: "DURI", - }, - ], - }, - }, - ), - }, - - detail: { - description: - "Get a list of schedule data for a train route from a train ID sorted by timeEstimated", - tags: ["Route"], - }, - }, - ) - - return app - }) - -export default routeController diff --git a/src/controllers/schedule.ts b/src/controllers/schedule.ts deleted file mode 100644 index f84af34..0000000 --- a/src/controllers/schedule.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Elysia, InternalServerError, t } from "elysia" -import * as service from "../services" -import { SyncType, syncResponse } from "../commons/types" - -const scheduleController = (app: Elysia) => - app.group("/schedule", (app) => { - app.post( - "/", - async (ctx) => { - const type: SyncType = ctx.query.from_cron ? "cron" : "manual" - - if (process.env.NODE_ENV === "development") { - return await service.schedule.sync(type) - } - const token = ctx.headers.authorization - - if (!token) { - throw new InternalServerError("Please provide a token") - } - - if (token.split(" ")[1] !== process.env.SYNC_TOKEN) { - throw new InternalServerError("Invalid token") - } - - return await service.schedule.sync(type) - }, - { - headers: - process.env.NODE_ENV === "development" - ? undefined - : t.Object({ - authorization: t.String(), - }), - query: t.Object({ - from_cron: t.Optional(t.BooleanString()), - }), - detail: { - description: "Sync schedule data", - tags: ["Schedule"], - }, - response: syncResponse("schedule"), - }, - ) - - app.get( - "/:stationId", - async (ctx) => { - return await service.schedule.getAll( - ctx.params.stationId.toLocaleUpperCase(), - ctx.query.is_from_now ?? false, - ) - }, - { - params: t.Object({ - stationId: t.String(), - }), - query: t.Object({ - is_from_now: t.Optional(t.BooleanString()), - }), - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Schedule data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Array( - t.Object({ - id: t.Nullable(t.String()), - stationId: t.Nullable(t.String()), - trainId: t.Nullable(t.String()), - line: t.Nullable(t.String()), - route: t.Nullable(t.String()), - color: t.Nullable(t.String()), - destination: t.Nullable(t.String()), - timeEstimated: t.Nullable(t.String()), - destinationTime: t.Nullable(t.String()), - updatedAt: t.Nullable(t.String()), - }), - ), - }, - { - default: { - status: 200, - data: [ - { - id: "AC-2400", - stationId: "AC", - trainId: "2400", - line: "COMMUTER LINE TANJUNGPRIUK", - route: "JAKARTAKOTA-TANJUNGPRIUK", - color: "#DD0067", - destination: "TANJUNGPRIUK", - timeEstimated: "06:07:00", - destinationTime: "06:16:00", - updatedAt: "2024-03-09T13:06:10.662Z", - }, - { - id: "AC-2401", - stationId: "AC", - trainId: "2401", - line: "COMMUTER LINE TANJUNGPRIUK", - route: "TANJUNGPRIUK-JAKARTAKOTA", - color: "#DD0067", - destination: "JAKARTAKOTA", - timeEstimated: "06:34:00", - destinationTime: "06:42:00", - updatedAt: "2024-03-09T13:06:10.662Z", - }, - ], - }, - }, - ), - }, - - detail: { - description: - "Get a list of schedule data for a station from a station ID", - tags: ["Schedule"], - }, - }, - ) - - return app - }) - -export default scheduleController diff --git a/src/controllers/station.ts b/src/controllers/station.ts deleted file mode 100644 index 85f2176..0000000 --- a/src/controllers/station.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Elysia, InternalServerError, t } from "elysia" -import * as service from "../services" -import { - SyncType, - scheduleResponseObject, - syncResponse, -} from "../commons/types" - -const stationController = (app: Elysia) => - app.group("/station", (app) => { - app.post( - "/", - async (ctx) => { - const type: SyncType = ctx.query.from_cron ? "cron" : "manual" - - if (process.env.NODE_ENV === "development") - return await service.station.sync(type) - - const token = ctx.headers.authorization - - if (!token) throw new InternalServerError("Please provide a token") - - if (token.split(" ")[1] !== process.env.SYNC_TOKEN) - throw new InternalServerError("Invalid token") - - return await service.station.sync(type) - }, - { - headers: - process.env.NODE_ENV === "development" - ? undefined - : t.Object({ - authorization: t.Nullable(t.String()), - }), - query: t.Object({ - from_cron: t.Optional(t.BooleanString()), - }), - detail: { - description: "Sync station data", - tags: ["Station"], - }, - response: syncResponse("station"), - }, - ) - - app.get( - "/", - async (ctx) => { - return await service.station.getAll() - }, - { - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Station data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Array(t.Object(scheduleResponseObject)), - }, - { - default: { - status: 200, - data: [ - { - id: "AC", - name: "ANCOL", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - { - id: "AK", - name: "ANGKE", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - ], - }, - }, - ), - }, - detail: { - description: "Get a list of station data", - tags: ["Station"], - }, - }, - ) - - app.get( - "/:id", - async (ctx) => { - if (!ctx.params.id) - throw new InternalServerError("Station ID is required") - - return await service.station.getById(ctx.params.id.toLocaleUpperCase()) - }, - { - params: t.Object({ - id: t.String(), - }), - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Station data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Object(scheduleResponseObject), - }, - { - default: { - status: 200, - data: { - id: "AC", - name: "ANCOL", - daop: 1, - fgEnable: 1, - haveSchedule: true, - updatedAt: "2024-03-10T09:55:07.213Z", - }, - }, - }, - ), - }, - detail: { - description: "Get a station data from a station ID", - tags: ["Station"], - }, - }, - ) - - return app - }) - -export default stationController diff --git a/src/controllers/sync.ts b/src/controllers/sync.ts deleted file mode 100644 index 31d4123..0000000 --- a/src/controllers/sync.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Elysia, t } from "elysia" -import { syncResponseObject } from "../commons/types" -import * as service from "../services" - -const syncController = (app: Elysia) => - app.group("/sync", (app) => { - app.get( - "/", - async () => { - return await service.sync.getAll() - }, - { - response: { - 200: t.Object( - { - status: t.Number(), - data: t.Array(t.Object(syncResponseObject)), - }, - { - default: { - status: 200, - data: [ - { - id: "2bb72322-7152-4b79-bea5-e3f639a71501", - n: 12, - type: "manual", - status: "FAILED", - item: "station", - duration: 12, - message: "Not implemented", - startedAt: "2024-03-10 12:19:24.500629+00", - endedAt: "2024-03-10T12:19:24.515Z", - createdAt: "2024-03-10 12:19:24.500629+00", - }, - { - id: "5f3523fe-b56b-4306-8498-a160588c2839", - n: 11, - type: "manual", - status: "SUCCESS", - item: "station", - duration: 11, - message: null, - startedAt: "2024-03-10 12:19:24.500629+00", - endedAt: "2024-03-10T12:19:24.515Z", - createdAt: "2024-03-10 12:19:24.500629+00", - }, - ], - }, - }, - ), - }, - - detail: { - description: "Get the most updated 20 sync data", - tags: ["Utility"], - }, - }, - ) - - app.get( - "/:id", - async (ctx) => { - return await service.sync.getItemById(ctx.params.id) - }, - { - params: t.Object({ - id: t.String(), - }), - - response: { - 404: t.Object( - { - status: t.Number(), - message: t.String(), - }, - { - default: { - status: 404, - message: "Sync data is not found", - }, - }, - ), - 200: t.Object( - { - status: t.Number(), - data: t.Object(syncResponseObject), - }, - { - default: { - status: 200, - data: { - id: "5f3523fe-b56b-4306-8498-a160588c2839", - n: 11, - type: "manual", - status: "SUCCESS", - item: "station", - duration: 11, - message: null, - startedAt: "2024-03-10 12:19:24.500629+00", - endedAt: "2024-03-10T12:19:24.515Z", - createdAt: "2024-03-10 12:19:24.500629+00", - }, - }, - }, - ), - }, - - detail: { - description: "Get a sync data item", - tags: ["Utility"], - }, - }, - ) - - return app - }) - -export default syncController diff --git a/src/db/index.ts b/src/db/index.ts deleted file mode 100644 index b70bf70..0000000 --- a/src/db/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js" -import dbConnection from "../commons/libs/db" - -import { station, schedule, sync } from "./schema" - -const dbSchema = { - station, - schedule, - sync, -} - -const db = drizzle(dbConnection, { - schema: dbSchema, -}) - -export { dbSchema, db } diff --git a/src/db/migrate.ts b/src/db/migrate.ts index 6f70c64..6543c23 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.ts @@ -1,16 +1,18 @@ +import { config } from "dotenv" +import { drizzle } from "drizzle-orm/postgres-js" import { migrate } from "drizzle-orm/postgres-js/migrator" -import { logger } from "../commons/utils/log" -import { db } from "./index" +import postgres from "postgres" -// https://orm.drizzle.team/docs/migrations +config({ path: ".dev.vars" }) -try { - // This will run migrations on the database, skipping the ones already applied - await migrate(db, { migrationsFolder: "./src/db/migrations" }) +const url = `${process.env.DATABASE_URL}` +const db = drizzle(postgres(url)) - logger.info("Migration success") - process.exit(0) -} catch (error) { - logger.error(`Migration error: ${error}`) +const main = async () => { + console.info("Migrating database") + await migrate(db, { migrationsFolder: "drizzle/migrations" }) + console.log("Migration complete") process.exit(0) } + +main() diff --git a/src/db/migrations/0000_futuristic_colossus.sql b/src/db/migrations/0000_futuristic_colossus.sql deleted file mode 100644 index 078434d..0000000 --- a/src/db/migrations/0000_futuristic_colossus.sql +++ /dev/null @@ -1,23 +0,0 @@ -CREATE TABLE IF NOT EXISTS "schedule" ( - "id" text PRIMARY KEY NOT NULL, - "station_id" text DEFAULT NULL, - "train_id" text DEFAULT NULL, - "line" text DEFAULT NULL, - "route" text DEFAULT NULL, - "color" text DEFAULT NULL, - "destination" text DEFAULT NULL, - "time_estimated" time DEFAULT NULL, - "destination_time" time DEFAULT NULL, - "updated_at" text DEFAULT (CURRENT_TIMESTAMP), - CONSTRAINT "schedule_id_unique" UNIQUE("id") -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "station" ( - "id" text PRIMARY KEY NOT NULL, - "name" text DEFAULT NULL, - "daop" integer DEFAULT NULL, - "fg_enable" integer DEFAULT NULL, - "have_schedule" boolean DEFAULT true, - "updated_at" text DEFAULT (CURRENT_TIMESTAMP), - CONSTRAINT "station_id_unique" UNIQUE("id") -); diff --git a/src/db/migrations/0001_dapper_the_professor.sql b/src/db/migrations/0001_dapper_the_professor.sql deleted file mode 100644 index aba4df6..0000000 --- a/src/db/migrations/0001_dapper_the_professor.sql +++ /dev/null @@ -1,31 +0,0 @@ -DO $$ BEGIN - CREATE TYPE "sync_from" AS ENUM('cron', 'manual'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - CREATE TYPE "sync_item" AS ENUM('station', 'schedule'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - CREATE TYPE "sync_status" AS ENUM('PENDING', 'SUCCESS', 'FAILED'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "sync" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "n" bigserial NOT NULL, - "type" "sync_from" DEFAULT 'manual', - "status" "sync_status" DEFAULT 'PENDING', - "item" "sync_item", - "duration" bigint DEFAULT 0, - "message" text DEFAULT NULL, - "started_at" text DEFAULT (CURRENT_TIMESTAMP), - "ended_at" text DEFAULT NULL, - "created_at" text DEFAULT (CURRENT_TIMESTAMP), - CONSTRAINT "sync_id_unique" UNIQUE("id") -); diff --git a/src/db/migrations/0002_smart_vermin.sql b/src/db/migrations/0002_smart_vermin.sql deleted file mode 100644 index db5b6c2..0000000 --- a/src/db/migrations/0002_smart_vermin.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX IF NOT EXISTS "station_idx" ON "schedule" ("station_id"); \ No newline at end of file diff --git a/src/db/migrations/0003_confused_lethal_legion.sql b/src/db/migrations/0003_confused_lethal_legion.sql deleted file mode 100644 index 74f14f3..0000000 --- a/src/db/migrations/0003_confused_lethal_legion.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX IF NOT EXISTS "time_estimated_idx" ON "schedule" ("time_estimated"); \ No newline at end of file diff --git a/src/db/migrations/meta/0000_snapshot.json b/src/db/migrations/meta/0000_snapshot.json deleted file mode 100644 index 52e356c..0000000 --- a/src/db/migrations/meta/0000_snapshot.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "id": "3971c9c2-bffe-4208-bf4a-20601164fd08", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "5", - "dialect": "pg", - "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - } - }, - "enums": {}, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json deleted file mode 100644 index 2f410ac..0000000 --- a/src/db/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,264 +0,0 @@ -{ - "id": "4e205dc5-b3ad-4093-989f-1aa9745b846e", - "prevId": "3971c9c2-bffe-4208-bf4a-20601164fd08", - "version": "5", - "dialect": "pg", - "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "sync": { - "name": "sync", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "n": { - "name": "n", - "type": "bigserial", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "sync_from", - "primaryKey": false, - "notNull": false, - "default": "'manual'" - }, - "status": { - "name": "status", - "type": "sync_status", - "primaryKey": false, - "notNull": false, - "default": "'PENDING'" - }, - "item": { - "name": "item", - "type": "sync_item", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "started_at": { - "name": "started_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "ended_at": { - "name": "ended_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sync_id_unique": { - "name": "sync_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - } - }, - "enums": { - "sync_from": { - "name": "sync_from", - "values": { - "cron": "cron", - "manual": "manual" - } - }, - "sync_item": { - "name": "sync_item", - "values": { - "station": "station", - "schedule": "schedule" - } - }, - "sync_status": { - "name": "sync_status", - "values": { - "PENDING": "PENDING", - "SUCCESS": "SUCCESS", - "FAILED": "FAILED" - } - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/src/db/migrations/meta/0002_snapshot.json b/src/db/migrations/meta/0002_snapshot.json deleted file mode 100644 index 8151826..0000000 --- a/src/db/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "id": "64239a74-574c-46d7-b0d1-e0285ce4bd81", - "prevId": "4e205dc5-b3ad-4093-989f-1aa9745b846e", - "version": "5", - "dialect": "pg", - "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": { - "station_idx": { - "name": "station_idx", - "columns": ["station_id"], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "sync": { - "name": "sync", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "n": { - "name": "n", - "type": "bigserial", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "sync_from", - "primaryKey": false, - "notNull": false, - "default": "'manual'" - }, - "status": { - "name": "status", - "type": "sync_status", - "primaryKey": false, - "notNull": false, - "default": "'PENDING'" - }, - "item": { - "name": "item", - "type": "sync_item", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "started_at": { - "name": "started_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "ended_at": { - "name": "ended_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sync_id_unique": { - "name": "sync_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - } - }, - "enums": { - "sync_from": { - "name": "sync_from", - "values": { - "cron": "cron", - "manual": "manual" - } - }, - "sync_item": { - "name": "sync_item", - "values": { - "station": "station", - "schedule": "schedule" - } - }, - "sync_status": { - "name": "sync_status", - "values": { - "PENDING": "PENDING", - "SUCCESS": "SUCCESS", - "FAILED": "FAILED" - } - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/src/db/migrations/meta/0003_snapshot.json b/src/db/migrations/meta/0003_snapshot.json deleted file mode 100644 index e3d862a..0000000 --- a/src/db/migrations/meta/0003_snapshot.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "id": "a483bfd1-dad0-4549-98bb-58a0196f6440", - "prevId": "64239a74-574c-46d7-b0d1-e0285ce4bd81", - "version": "5", - "dialect": "pg", - "tables": { - "schedule": { - "name": "schedule", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "station_id": { - "name": "station_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "train_id": { - "name": "train_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "line": { - "name": "line", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "route": { - "name": "route", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination": { - "name": "destination", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "time_estimated": { - "name": "time_estimated", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "destination_time": { - "name": "destination_time", - "type": "time", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": { - "station_idx": { - "name": "station_idx", - "columns": ["station_id"], - "isUnique": false - }, - "time_estimated_idx": { - "name": "time_estimated_idx", - "columns": ["time_estimated"], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "schedule_id_unique": { - "name": "schedule_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "station": { - "name": "station", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "daop": { - "name": "daop", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "fg_enable": { - "name": "fg_enable", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "have_schedule": { - "name": "have_schedule", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": "true" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "station_id_unique": { - "name": "station_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - }, - "sync": { - "name": "sync", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "n": { - "name": "n", - "type": "bigserial", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "sync_from", - "primaryKey": false, - "notNull": false, - "default": "'manual'" - }, - "status": { - "name": "status", - "type": "sync_status", - "primaryKey": false, - "notNull": false, - "default": "'PENDING'" - }, - "item": { - "name": "item", - "type": "sync_item", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "started_at": { - "name": "started_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "ended_at": { - "name": "ended_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "NULL" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sync_id_unique": { - "name": "sync_id_unique", - "nullsNotDistinct": false, - "columns": ["id"] - } - } - } - }, - "enums": { - "sync_from": { - "name": "sync_from", - "values": { - "cron": "cron", - "manual": "manual" - } - }, - "sync_item": { - "name": "sync_item", - "values": { - "station": "station", - "schedule": "schedule" - } - }, - "sync_status": { - "name": "sync_status", - "values": { - "PENDING": "PENDING", - "SUCCESS": "SUCCESS", - "FAILED": "FAILED" - } - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json deleted file mode 100644 index e8a85ba..0000000 --- a/src/db/migrations/meta/_journal.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "5", - "dialect": "pg", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1709969312190, - "tag": "0000_futuristic_colossus", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1709976152377, - "tag": "0001_dapper_the_professor", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1710153775566, - "tag": "0002_smart_vermin", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1710159697753, - "tag": "0003_confused_lethal_legion", - "breakpoints": true - } - ] -} diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 732f3c3..6d21403 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -1,76 +1,2 @@ -import { sql } from "drizzle-orm" -import { - bigint, - bigserial, - boolean, - date, - index, - integer, - pgEnum, - pgTable, - text, - time, - uuid, -} from "drizzle-orm/pg-core" - -export const schedule = pgTable( - "schedule", - { - id: text("id").primaryKey().unique(), - stationId: text("station_id").default(sql`NULL`), - trainId: text("train_id").default(sql`NULL`), - line: text("line").default(sql`NULL`), - route: text("route").default(sql`NULL`), - color: text("color").default(sql`NULL`), - destination: text("destination").default(sql`NULL`), - timeEstimated: time("time_estimated").default(sql`NULL`), - destinationTime: time("destination_time").default(sql`NULL`), - updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`), - }, - (table) => { - return { - stationIdx: index("station_idx").on(table.stationId), - timeEstimatedIdx: index("time_estimated_idx").on(table.timeEstimated), - } - }, -) - -export type Schedule = typeof schedule.$inferSelect - -export const station = pgTable("station", { - id: text("id").primaryKey().unique(), - name: text("name").default(sql`NULL`), - daop: integer("daop").default(sql`NULL`), - fgEnable: integer("fg_enable").default(sql`NULL`), - haveSchedule: boolean("have_schedule").default(sql`true`), - updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`), -}) - -export type Station = typeof station.$inferSelect -export type NewStation = typeof station.$inferInsert - -export const syncFromEnum = pgEnum("sync_from", ["cron", "manual"]) -export const syncStatusEnum = pgEnum("sync_status", [ - "PENDING", - "SUCCESS", - "FAILED", -]) - -export const syncItemEnum = pgEnum("sync_item", ["station", "schedule"]) - -export const sync = pgTable("sync", { - id: uuid("id").defaultRandom().primaryKey().unique(), - n: bigserial("n", { mode: "number" }), - type: syncFromEnum("type").default("manual"), - status: syncStatusEnum("status").default("PENDING"), - item: syncItemEnum("item"), - duration: bigint("duration", { - mode: "number", - }).default(0), - message: text("message").default(sql`NULL`), - startedAt: text("started_at").default(sql`(CURRENT_TIMESTAMP)`), - endedAt: text("ended_at").default(sql`NULL`), - createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`), -}) - -export type NewSync = typeof sync.$inferInsert +export * from "./station.table" +export * from "./schedule.table" diff --git a/src/db/schema/schedule.table.ts b/src/db/schema/schedule.table.ts new file mode 100644 index 0000000..04cbd9e --- /dev/null +++ b/src/db/schema/schedule.table.ts @@ -0,0 +1,102 @@ +import { + index, + jsonb, + pgTable, + text, + time, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" +import { createSelectSchema } from "drizzle-zod" +import { z } from "zod" +import { stationTable } from "./station.table" +import { relations } from "drizzle-orm" + +export const stationScheduleMetadata = z.object({ + /** Origin metadata */ + origin: z.object({ + color: z.string().nullable(), + }), +}) + +export type StationScheduleMetadata = z.infer + +export const scheduleTable = pgTable( + "schedule", + { + id: text("id").primaryKey().unique().notNull(), + station_id: text("station_id") + .notNull() + .references(() => stationTable.id, { + onDelete: "cascade", + }), + station_origin_id: text("station_origin_id") + .references(() => stationTable.id, { + onDelete: "set null", + }) + .notNull(), + station_destination_id: text("station_destination_id") + .references(() => stationTable.id, { + onDelete: "set null", + }) + .notNull(), + train_id: text("train_id").notNull(), + line: text("line").notNull(), + route: text("route").notNull(), + departs_at: timestamp("departs_at", { + mode: "string", + withTimezone: true, + }) + .notNull() + .defaultNow(), + arrives_at: timestamp("arrives_at", { + mode: "string", + withTimezone: true, + }) + .notNull() + .defaultNow(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + mode: "string", + withTimezone: true, + }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + }, + (table) => { + return { + schedule_idx: uniqueIndex("schedule_idx").on(table.id), + schedule_station_idx: index("schedule_station_idx").on(table.station_id), + schedule_train_idx: index("schedule_train_idx").on(table.train_id), + } + }, +) + +export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ + station: one(stationTable, { + fields: [scheduleTable.station_id], + references: [stationTable.id], + }), + station_origin: one(stationTable, { + fields: [scheduleTable.station_origin_id], + references: [stationTable.id], + }), + station_destination: one(stationTable, { + fields: [scheduleTable.station_destination_id], + references: [stationTable.id], + }), +})) + +export const scheduleSchema = createSelectSchema(scheduleTable, { + metadata: stationScheduleMetadata.nullable(), +}) + +export type Schedule = z.infer + +export type NewSchedule = typeof scheduleTable.$inferInsert diff --git a/src/db/schema/station.table.ts b/src/db/schema/station.table.ts new file mode 100644 index 0000000..d211b1b --- /dev/null +++ b/src/db/schema/station.table.ts @@ -0,0 +1,70 @@ +import { + index, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" +import { createSelectSchema } from "drizzle-zod" +import { z } from "zod" + +/** Station Metadata */ +const stationMetadata = z.object({ + /** Comuline metadata */ + active: z.boolean().optional(), + /** Origin metadata */ + origin: z.object({ + /** KRL */ + daop: z.number().nullable(), + fg_enable: z.number().nullable(), + }), +}) + +export type StationMetadata = z.infer + +export const stationTypeEnum = pgEnum("station_type", [ + "KRL", + "MRT", + "LRT", + "LOCAL", +]) + +export const stationTable = pgTable( + "station", + { + uid: text("uid").primaryKey().unique().notNull(), + id: text("id").unique().notNull(), + name: text("name").notNull(), + type: stationTypeEnum("type").notNull(), + metadata: jsonb("metadata").$type(), + created_at: timestamp("created_at", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + }, + (table) => { + return { + station_uidx: uniqueIndex("station_uidx").on(table.uid), + station_idx: index("station_idx").on(table.id), + type_idx: index("station_type_idx").on(table.type), + } + }, +) + +export const stationSchema = createSelectSchema(stationTable) + +export type NewStation = typeof stationTable.$inferInsert + +export type Station = z.infer + +export type StationType = Station["type"] diff --git a/src/index.ts b/src/index.ts index 9568f9e..2071d8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,32 +1,83 @@ -import { Elysia } from "elysia" -import controllers from "./controllers" -import { logger } from "./commons/utils/log" -import swagger from "./commons/libs/swagger" -import { rateLimit } from "elysia-rate-limit" +import { apiReference } from "@scalar/hono-api-reference" +import { createAPI } from "./modules/api" +import v1 from "./modules/v1" +import { Database } from "./modules/v1/database" +import { HTTPException } from "hono/http-exception" +import { constructResponse } from "./utils/response" +import { trimTrailingSlash } from "hono/trailing-slash" +import { cors } from "hono/cors" +const api = createAPI() -const app = new Elysia() - .use(controllers) - .get("/", (ctx) => { - ctx.set.redirect = "/docs" +const app = api + .doc("/openapi", (c) => ({ + openapi: "3.0.0", + info: { + version: "1.0.0", + title: "Comuline API", + }, + servers: [ + { + url: new URL(c.req.url).origin, + description: c.env.COMULINE_ENV, + }, + ], + })) + .use(trimTrailingSlash()) + .use("*", async (c, next) => + cors({ + origin: (o) => o, + allowMethods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"], + allowHeaders: ["Origin", "Content-Type"], + credentials: true, + })(c, next), + ) + .use(async (c, next) => { + const { db } = new Database({ + COMULINE_ENV: c.env.COMULINE_ENV, + DATABASE_URL: c.env.DATABASE_URL, + }) + c.set("db", db) + c.set("constructResponse", constructResponse) + await next() }) - .get("/health", () => { - return { - status: 200, - data: { - message: "OK", + .route("/v1", v1) + .use( + "/docs", + apiReference({ + cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference", + spec: { + url: "/openapi", }, + }), + ) + .get("/", (c) => c.redirect("/docs")) + .get("/status", (c) => c.json({ status: "ok" })) + .notFound(() => { + throw new HTTPException(404, { message: "Not found" }) + }) + .onError((err, c) => { + if (err instanceof HTTPException) { + return c.json( + { + metadata: { + success: false, + message: err.message, + cause: err.cause, + }, + }, + err.status, + ) } + return c.json( + { + metadata: { + success: false, + message: err.message, + cause: err.cause, + }, + }, + 500, + ) }) - .use(swagger()) - .use(rateLimit({ max: 5 })) - -try { - app.listen(process.env.NODE_ENV === "development" ? 3001 : 3000) -} catch (e) { - logger.error("[MAIN] Error starting server", e) - process.exit(1) -} -logger.info( - `[MAIN] Service is running at ${app.server?.hostname}:${app.server?.port}`, -) +export default app diff --git a/src/modules/api.ts b/src/modules/api.ts new file mode 100644 index 0000000..f78c1cd --- /dev/null +++ b/src/modules/api.ts @@ -0,0 +1,5 @@ +import { OpenAPIHono } from "@hono/zod-openapi" +import { type Environments } from "@/type" + +export const createAPI = () => + new OpenAPIHono({ strict: true }) diff --git a/src/modules/v1/cache.ts b/src/modules/v1/cache.ts new file mode 100644 index 0000000..8dc673f --- /dev/null +++ b/src/modules/v1/cache.ts @@ -0,0 +1,34 @@ +import { Redis } from "@upstash/redis/cloudflare" + +export class Cache { + protected kv: Redis + public key: string + + constructor( + protected env: { + UPSTASH_REDIS_REST_TOKEN: string + UPSTASH_REDIS_REST_URL: string + }, + key: string, + ) { + this.key = key + this.kv = Redis.fromEnv(env) + } + + async get(): Promise { + const data = await this.kv.get(this.key) + return data ?? null + } + + async set(value: T, ttl?: number): Promise { + await this.kv.set( + this.key, + JSON.stringify(value), + ttl + ? { + ex: ttl, + } + : undefined, + ) + } +} diff --git a/src/modules/v1/database.ts b/src/modules/v1/database.ts new file mode 100644 index 0000000..c0dd86a --- /dev/null +++ b/src/modules/v1/database.ts @@ -0,0 +1,28 @@ +import { neonConfig, Pool } from "@neondatabase/serverless" +import { drizzle, NeonDatabase } from "drizzle-orm/neon-serverless" +import * as schema from "@/db/schema" + +export class Database< + T extends { + DATABASE_URL: string + COMULINE_ENV: string + }, +> { + db: NeonDatabase + + constructor(protected env: T) { + this.db = connectDB(env.DATABASE_URL, env.COMULINE_ENV) + } +} + +export const connectDB = (url: string, env: string) => { + if (env === "development") { + neonConfig.wsProxy = (host) => `${host}:5433/v1` + neonConfig.useSecureWebSocket = false + neonConfig.pipelineTLS = false + neonConfig.pipelineConnect = false + } + + const pool = new Pool({ connectionString: url, ssl: true }) + return drizzle(pool, { schema }) +} diff --git a/src/modules/v1/index.ts b/src/modules/v1/index.ts new file mode 100644 index 0000000..a5460b0 --- /dev/null +++ b/src/modules/v1/index.ts @@ -0,0 +1,13 @@ +import { createAPI } from "../api" +import routeController from "./route/route.controller" +import scheduleController from "./schedule/schedule.controller" +import stationController from "./station/station.controller" + +const api = createAPI() + +const v1 = api + .route("/station", stationController) + .route("/schedule", scheduleController) + .route("/route", routeController) + +export default v1 diff --git a/src/modules/v1/route/route.controller.ts b/src/modules/v1/route/route.controller.ts new file mode 100644 index 0000000..7ab9264 --- /dev/null +++ b/src/modules/v1/route/route.controller.ts @@ -0,0 +1,131 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { eq, sql } from "drizzle-orm" +import { scheduleTable } from "@/db/schema" +import { buildResponseSchemas } from "@/utils/response" +import { getSecsToMidnight } from "@/utils/time" +import { createAPI } from "@/modules/api" +import { Cache } from "../cache" +import { Route, routeResponseSchema } from "./route.schema" + +const api = createAPI() + +const routeController = api.openapi( + createRoute({ + method: "get", + path: "/{train_id}", + request: { + params: z.object({ + train_id: z + .string() + .min(2) + .openapi({ + param: { + name: "train_id", + in: "path", + }, + default: "2400", + example: "2400", + }), + }), + }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: routeResponseSchema, + }, + ]), + tags: ["Route"], + description: "Get sequence of station stop by train ID", + }), + async (c) => { + const param = c.req.valid("param") + const { db } = c.var + + const cache = new Cache(c.env, `route:${param.train_id}`) + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(routeResponseSchema, cached), + }, + 200, + ) + + const query = db.query.scheduleTable + .findMany({ + with: { + station: { + columns: { + name: true, + }, + }, + station_destination: { + columns: { + name: true, + }, + }, + }, + orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.departs_at)], + where: eq(scheduleTable.train_id, sql.placeholder("train_id")), + }) + .prepare("query_route_by_train_id") + + const data = await query.execute({ + train_id: param.train_id, + }) + + if (data.length === 0) + return c.json( + { + metadata: { + success: true, + }, + data: [], + }, + 200, + ) + + const response = { + routes: data.map( + ({ id, station_id, station, departs_at, created_at, updated_at }) => ({ + id, + station_id, + station_name: station.name, + departs_at, + created_at, + updated_at, + }), + ), + details: { + train_id: param.train_id, + line: data[0].line, + route: data[0].route, + station_origin_id: data[0].station_origin_id, + station_origin_name: data[0].station.name, + station_destination_id: data[0].station_destination_id, + station_destination_name: data[0].station_destination?.name ?? "", + arrives_at: data[0].arrives_at, + }, + } satisfies Route + + await cache.set(response, getSecsToMidnight()) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(routeResponseSchema, response), + }, + 200, + ) + }, +) + +export default routeController diff --git a/src/modules/v1/route/route.schema.ts b/src/modules/v1/route/route.schema.ts new file mode 100644 index 0000000..ba075dc --- /dev/null +++ b/src/modules/v1/route/route.schema.ts @@ -0,0 +1,37 @@ +import { z } from "@hono/zod-openapi" +import { scheduleResponseSchema } from "../schedule/schedule.schema" +import { stationResponseSchema } from "../station/station.schema" + +export const routeResponseSchema = z + .object({ + routes: z.array( + z.object({ + id: scheduleResponseSchema.shape.id, + station_id: scheduleResponseSchema.shape.station_id, + station_name: stationResponseSchema.shape.name.openapi({ + example: "ANCOL", + }), + departs_at: scheduleResponseSchema.shape.departs_at, + created_at: scheduleResponseSchema.shape.created_at, + updated_at: scheduleResponseSchema.shape.updated_at, + }), + ), + details: z.object({ + train_id: scheduleResponseSchema.shape.train_id, + line: scheduleResponseSchema.shape.line, + route: scheduleResponseSchema.shape.route, + station_origin_id: scheduleResponseSchema.shape.station_origin_id, + station_origin_name: stationResponseSchema.shape.name.openapi({ + example: "JAKARTAKOTA", + }), + station_destination_id: + scheduleResponseSchema.shape.station_destination_id, + station_destination_name: z.string().optional().openapi({ + example: "TANJUNGPRIUK", + }), + arrives_at: scheduleResponseSchema.shape.arrives_at, + }), + }) + .openapi("Route") + +export type Route = z.infer diff --git a/src/modules/v1/schedule/schedule.controller.ts b/src/modules/v1/schedule/schedule.controller.ts new file mode 100644 index 0000000..713887e --- /dev/null +++ b/src/modules/v1/schedule/schedule.controller.ts @@ -0,0 +1,96 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { asc, eq, sql } from "drizzle-orm" +import { scheduleTable, Schedule } from "@/db/schema" +import { createAPI } from "@/modules/api" +import { buildResponseSchemas } from "@/utils/response" +import { scheduleResponseSchema } from "./schedule.schema" +import { Cache } from "../cache" +import { getSecsToMidnight } from "@/utils/time" + +const api = createAPI() + +const scheduleController = api.openapi( + createRoute({ + method: "get", + path: "/{station_id}", + request: { + params: z.object({ + station_id: z + .string() + .min(2) + .openapi({ + param: { + name: "station_id", + in: "path", + }, + default: "AC", + example: "AC", + }), + }), + }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: z.array(scheduleResponseSchema), + }, + ]), + tags: ["Schedule"], + description: "Get all schedule by station ID", + }), + async (c) => { + const param = c.req.valid("param") + const { db } = c.var + + const cache = new Cache>( + c.env, + `schedules:${param.station_id}`, + ) + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(scheduleResponseSchema), + cached, + ), + }, + 200, + ) + + const query = db + .select() + .from(scheduleTable) + .where(eq(scheduleTable.station_id, sql.placeholder("station_id"))) + .orderBy(asc(scheduleTable.departs_at)) + .prepare("query_schedule_by_station_id") + + const data = await query.execute({ + station_id: param.station_id.toLocaleUpperCase(), + }) + + await cache.set(data, getSecsToMidnight()) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(scheduleResponseSchema), + data.map((x) => { + return x + }), + ), + }, + 200, + ) + }, +) + +export default scheduleController diff --git a/src/modules/v1/schedule/schedule.schema.ts b/src/modules/v1/schedule/schedule.schema.ts new file mode 100644 index 0000000..dac67b6 --- /dev/null +++ b/src/modules/v1/schedule/schedule.schema.ts @@ -0,0 +1,74 @@ +import { scheduleSchema, StationScheduleMetadata } from "@/db/schema" +import { z } from "@hono/zod-openapi" + +export const scheduleResponseSchema = z + .object({ + id: scheduleSchema.shape.id.openapi({ + example: "sc_krl_ac_2400", + description: "Schedule unique ID", + }), + station_id: scheduleSchema.shape.station_id.openapi({ + example: "AC", + description: "Station ID where the train stops", + }), + station_origin_id: scheduleSchema.shape.station_origin_id.openapi({ + example: "JAKK", + description: "Station ID where the train originates", + }), + station_destination_id: scheduleSchema.shape.station_destination_id.openapi( + { + example: "TPK", + description: "Station ID where the train terminates", + }, + ), + train_id: scheduleSchema.shape.train_id.openapi({ + example: "2400", + description: "Train ID", + }), + line: scheduleSchema.shape.line.openapi({ + example: "COMMUTER LINE TANJUNGPRIUK", + description: "Train line", + }), + route: scheduleSchema.shape.route.openapi({ + example: "JAKARTAKOTA-TANJUNGPRIUK", + description: "Train route", + }), + departs_at: scheduleSchema.shape.departs_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + description: "Train departure time", + }), + arrives_at: scheduleSchema.shape.arrives_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:09.213Z", + description: "Train arrival time at destination", + }), + metadata: scheduleSchema.shape.metadata.openapi({ + type: "object", + properties: { + origin: { + type: "object", + properties: { + color: { + type: "string", + nullable: true, + }, + }, + }, + }, + example: { + origin: { + color: "#DD0067", + }, + } satisfies StationScheduleMetadata, + }), + created_at: scheduleSchema.shape.created_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + }), + updated_at: scheduleSchema.shape.updated_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + }), + }) + .openapi("Schedule") satisfies typeof scheduleSchema diff --git a/src/modules/v1/station/station.controller.ts b/src/modules/v1/station/station.controller.ts new file mode 100644 index 0000000..3e4e2e5 --- /dev/null +++ b/src/modules/v1/station/station.controller.ts @@ -0,0 +1,154 @@ +import { createRoute, z } from "@hono/zod-openapi" +import { eq, sql } from "drizzle-orm" +import { Station, stationTable } from "@/db/schema" +import { buildResponseSchemas } from "@/utils/response" +import { getSecsToMidnight } from "@/utils/time" +import { createAPI } from "@/modules/api" +import { Cache } from "../cache" +import { stationResponseSchema } from "./station.schema" + +const api = createAPI() + +const stationController = api + .openapi( + createRoute({ + method: "get", + path: "/", + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: z.array(stationResponseSchema), + }, + ]), + tags: ["Station"], + description: "Get all station data", + }), + async (c) => { + const { db } = c.var + + const cache = new Cache>(c.env, "stations") + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(stationResponseSchema), + cached, + ), + }, + 200, + ) + + const query = db.select().from(stationTable).prepare("query_all_stations") + + const stations = await query.execute() + + await cache.set(stations, getSecsToMidnight()) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse( + z.array(stationResponseSchema), + stations, + ), + }, + 200, + ) + }, + ) + .openapi( + createRoute({ + method: "get", + path: "/{id}", + request: { + params: z.object({ + id: z + .string() + .min(1) + .openapi({ + param: { + name: "id", + in: "path", + }, + default: "MRI", + example: "MRI", + }), + }), + }, + responses: buildResponseSchemas([ + { + status: 200, + type: "data", + schema: stationResponseSchema, + }, + { + status: 404, + type: "metadata", + }, + ]), + tags: ["Station"], + description: "Get station by ID", + }), + async (c) => { + const param = c.req.valid("param") + + const { db } = c.var + + const cache = new Cache(c.env, `station:${param.id}`) + + const cached = await cache.get() + + if (cached) + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(stationResponseSchema, cached), + }, + 200, + ) + + const query = db + .select() + .from(stationTable) + .where(eq(stationTable.id, sql.placeholder("id"))) + .prepare("query_station_by_id") + + const data = await query.execute({ id: param.id.toLocaleUpperCase() }) + + if (data.length === 0) + return c.json( + { + metadata: { + success: false, + message: "Station not found", + }, + }, + 404, + ) + + await cache.set(data[0], getSecsToMidnight()) + + return c.json( + { + metadata: { + success: true, + }, + data: c.var.constructResponse(stationResponseSchema, data[0]), + }, + 200, + ) + }, + ) + +export default stationController diff --git a/src/modules/v1/station/station.schema.ts b/src/modules/v1/station/station.schema.ts new file mode 100644 index 0000000..9e5e7bc --- /dev/null +++ b/src/modules/v1/station/station.schema.ts @@ -0,0 +1,53 @@ +import { z } from "@hono/zod-openapi" +import { type StationMetadata, stationSchema } from "@/db/schema" + +export const stationResponseSchema = z + .object({ + uid: stationSchema.shape.uid.openapi({ + example: "st_krl_mri", + }), + id: stationSchema.shape.id.openapi({ + example: "MRI", + }), + name: stationSchema.shape.name.openapi({ + example: "MANGGARAI", + }), + type: stationSchema.shape.type.openapi({ + type: "string", + example: "KRL", + }), + metadata: stationSchema.shape.metadata.openapi({ + type: "object", + properties: { + origin: { + type: "object", + properties: { + daop: { + type: "number", + nullable: true, + }, + fg_enable: { + type: "number", + nullable: true, + }, + }, + }, + }, + example: { + active: true, + origin: { + daop: 1, + fg_enable: 1, + }, + } satisfies StationMetadata, + }), + created_at: stationSchema.shape.created_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + }), + updated_at: stationSchema.shape.updated_at.openapi({ + format: "date-time", + example: "2024-03-10T09:55:07.213Z", + }), + }) + .openapi("Station") satisfies typeof stationSchema diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index 9104257..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./station" -export * from "./schedule" -export * from "./sync" -export * from "./route" diff --git a/src/services/route/get-all.ts b/src/services/route/get-all.ts deleted file mode 100644 index 559d7b4..0000000 --- a/src/services/route/get-all.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { and, asc, eq, gte, sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import Cache from "../../commons/utils/cache" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { db, dbSchema } from "../../db" -import { Schedule, Station } from "../../db/schema" -import { getSecondsRemainingFromNow } from "../../commons/utils/date" - -export const getAll = async (trainId: string) => { - try { - const cache = new Cache<(Schedule & { stationName: Station["name"] })[]>( - `route-${trainId}`, - { - ttl: getSecondsRemainingFromNow(), - }, - ) - - const cached = await cache.get() - - if (cached) return cached - - const result = await db - .select() - .from(dbSchema.schedule) - .leftJoin( - dbSchema.station, - eq(dbSchema.schedule.stationId, dbSchema.station.id), - ) - .where(eq(dbSchema.schedule.trainId, trainId)) - .orderBy(asc(dbSchema.schedule.timeEstimated)) - - const schedules = result.map((res) => ({ - ...res.schedule, - stationName: res.station?.name || null, - })) - // Add the last station schedule - schedules.push({ - ...schedules[0], - stationName: schedules[0].destination, - timeEstimated: schedules[0].destinationTime, - }) - - if (schedules.length === 0) { - logger.error(`[QUERY][ROUTE][${trainId}] Route data is not found`) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} - -export const getAllFrom = async (trainId: string, fromStationId?: string) => { - try { - const cache = new Cache<(Schedule & { stationName: Station["name"] })[]>( - `route-${trainId}-${fromStationId}`, - { - ttl: getSecondsRemainingFromNow(), - }, - ) - - const cached = await cache.get() - - if (cached) return cached - - const result = await db - .select() - .from(dbSchema.schedule) - .leftJoin( - dbSchema.station, - eq(dbSchema.schedule.stationId, dbSchema.station.id), - ) - .where( - and( - eq(dbSchema.schedule.trainId, trainId), - gte( - dbSchema.schedule.timeEstimated, - sql`( - SELECT time_estimated FROM schedule - WHERE station_id = ${fromStationId} AND train_id = ${trainId} - )`, - ), - ), - ) - .orderBy(asc(dbSchema.schedule.timeEstimated)) - - const schedules = result.map((res) => ({ - ...res.schedule, - stationName: res.station?.name || null, - })) - // Add the last station schedule - schedules.push({ - ...schedules[0], - stationName: schedules[0].destination, - timeEstimated: schedules[0].destinationTime, - }) - - if (schedules.length === 0) { - logger.error(`[QUERY][ROUTE][${trainId}] Route data is not found`) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/route/index.ts b/src/services/route/index.ts deleted file mode 100644 index 377a6c1..0000000 --- a/src/services/route/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NotFoundError } from "elysia" -import { getAll, getAllFrom } from "./get-all" - -export const route = { - getAll: async (trainId: string) => { - const routes = await getAll(trainId) - - if (!routes) { - throw new NotFoundError("Route data is not found") - } - - return { - status: 200, - data: routes, - } - }, - getAllFrom: async (trainId: string, fromStationId?: string) => { - const routes = await getAllFrom(trainId, fromStationId) - - if (!routes) { - throw new NotFoundError("Route data is not found") - } - - return { - status: 200, - data: routes, - } - }, -} diff --git a/src/services/schedule/get-all.ts b/src/services/schedule/get-all.ts deleted file mode 100644 index f453f5b..0000000 --- a/src/services/schedule/get-all.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { asc, eq, sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import Cache from "../../commons/utils/cache" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { db, dbSchema } from "../../db" -import { Schedule } from "../../db/schema" - -export const getAll = async (stationId: string) => { - try { - const cache = new Cache(`schedule-${stationId}`, { - ttl: - 60 * - new Date(Date.now()).getMinutes() * - new Date(Date.now()).getHours(), - }) - - const cached = await cache.get() - - if (cached) return cached - - const schedules = await db.query.schedule.findMany({ - where: eq(dbSchema.schedule.stationId, stationId), - orderBy: [asc(dbSchema.schedule.timeEstimated)], - }) - - if (schedules.length === 0) { - logger.error(`[QUERY][SCHEDULE][${stationId}] Schedule data is not found`) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} - -export const getAllFromNow = async (stationId: string) => { - try { - const now = new Date() - - const currentSecond = now.getSeconds() - - const minutes = now.getMinutes() - - const cache = new Cache(`schedule-${stationId}-${minutes}`, { - ttl: 60 - currentSecond, - }) - - const cached = await cache.get() - - if (cached) return cached - - const schedules = await db.query.schedule.findMany({ - where: sql`station_id = ${stationId} AND time_estimated > (CURRENT_TIME AT TIME ZONE 'Asia/Jakarta')::time`, - orderBy: [asc(dbSchema.schedule.timeEstimated)], - }) - - if (schedules.length === 0) { - logger.warn( - `[QUERY][SCHEDULE][${stationId}] Schedule data from now is not found`, - ) - return null - } - - await cache.set(schedules) - - return schedules - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/schedule/index.ts b/src/services/schedule/index.ts deleted file mode 100644 index 5b6d595..0000000 --- a/src/services/schedule/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NotFoundError } from "elysia" -import { SyncType } from "../../commons/types" -import { syncWrapper } from "../utils/sync" -import { getAll, getAllFromNow } from "./get-all" -import { sync as syncSchedule } from "./sync" - -export const schedule = { - sync: async (type: SyncType) => { - const data = await syncWrapper(syncSchedule, { - item: "schedule", - type, - })() - - return { - status: 200, - data, - } - }, - getAll: async (stationId: string, fromNow: boolean) => { - const schedules = fromNow - ? await getAllFromNow(stationId) - : await getAll(stationId) - - if (!schedules) { - throw new NotFoundError("Schedule data is not found") - } - - return { - status: 200, - data: schedules, - } - }, -} diff --git a/src/services/schedule/sync.ts b/src/services/schedule/sync.ts deleted file mode 100644 index 9c98f88..0000000 --- a/src/services/schedule/sync.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { eq, sql } from "drizzle-orm" -import { db, dbSchema } from "../../db" -import { parseTime } from "../../commons/utils/date" -import { logger } from "../../commons/utils/log" -import { z } from "zod" -import { NewStation } from "../../db/schema" -import { sleep } from "bun" -import { InternalServerError } from "elysia" -import { handleError } from "../../commons/utils/error" - -export const syncItem = async (id: string) => { - try { - const req = await fetch( - `https://api-partner.krl.co.id/krlweb/v1/schedule?stationid=${id}&timefrom=00:00&timeto=24:00`, - ).then((res) => res.json()) - - logger.info(`[SYNC][SCHEDULE][${id}] Fetched data from API`) - - const schema = z.object({ - status: z.number(), - data: z.array( - z.object({ - train_id: z.string(), - ka_name: z.string(), - route_name: z.string(), - dest: z.string(), - time_est: z.string(), - color: z.string(), - dest_time: z.string(), - }), - ), - }) - - if ((req as unknown as { status: number }).status === 404) { - logger.warn(`[SYNC][SCHEDULE][${id}] No schedule data found`) - - const payload: Partial = { - haveSchedule: false, - updatedAt: new Date().toISOString(), - } - - await db - .update(dbSchema.station) - .set(payload) - .where(eq(dbSchema.station.id, id)) - - logger.warn( - `[SYNC][SCHEDULE][${id}] Updated station schedule availability status`, - ) - } else if ((req as unknown as { status: number }).status === 200) { - const parsedData = schema.parse(req) - - const insert = await db - .insert(dbSchema.schedule) - .values( - parsedData.data.map((d) => { - return { - id: `${id}-${d.train_id}`, - stationId: id, - trainId: d.train_id, - line: d.ka_name, - route: d.route_name, - destination: d.dest, - timeEstimated: parseTime(d.time_est).toLocaleTimeString(), - destinationTime: parseTime(d.dest_time).toLocaleTimeString(), - color: d.color, - } - }), - ) - .onConflictDoUpdate({ - target: dbSchema.schedule.id, - set: { - timeEstimated: sql`excluded.time_estimated`, - destinationTime: sql`excluded.destination_time`, - color: sql`excluded.color`, - updatedAt: new Date().toISOString(), - }, - }) - .returning() - - logger.info(`[SYNC][SCHEDULE][${id}] Inserted ${insert.length} rows`) - } else { - logger.error( - `[SYNC][SCHEDULE][${id}] Error fetch schedule data. Trace: ${JSON.stringify( - req, - )}`, - ) - throw new Error("Failed to fetch schedule data for: " + id) - } - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} - -export const sync = async () => { - const stationsQuery = await db.query.station.findMany() - - const initialStations = await stationsQuery.map(({ id }) => id) - - if (initialStations.length === 0) { - const err = "No station data is existing. Please sync station data first." - logger.error("[SYNC][SCHEDULE] " + err) - throw new Error(err) - } - - const blacklistQuery = await db - .select({ - id: dbSchema.station.id, - }) - .from(dbSchema.station) - .where(eq(dbSchema.station.haveSchedule, false)) - - const blacklist = await blacklistQuery.map(({ id }) => id) - - const stations = - blacklist.length > 0 - ? initialStations.filter((s) => !blacklist.includes(s)) - : initialStations - - try { - logger.info("[SYNC][SCHEDULE] Syncing schedule data started") - const batchSizes = 5 - const totalBatches = Math.ceil(stations.length / batchSizes) - - for (let i = 0; i < totalBatches; i++) { - const start = i * batchSizes - const end = start + batchSizes - const batch = stations.slice(start, end) - - await Promise.allSettled( - batch.map(async (id) => { - await sleep(300) - await syncItem(id) - }), - ) - } - logger.info("[SYNC][SCHEDULE] Syncing schedule data finished") - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/station/get-all.ts b/src/services/station/get-all.ts deleted file mode 100644 index d5c96c8..0000000 --- a/src/services/station/get-all.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { asc, eq } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { db, dbSchema } from "../../db" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { Station } from "../../db/schema" -import Cache from "../../commons/utils/cache" - -export const getAll = async () => { - try { - const cache = new Cache("station-all", { - ttl: - 60 * - new Date(Date.now()).getMinutes() * - new Date(Date.now()).getHours(), - }) - - const cached = await cache.get() - - if (cached) return cached - - const stations = await db.query.station.findMany({ - orderBy: [ - asc(dbSchema.station.id), - asc(dbSchema.station.daop), - asc(dbSchema.station.name), - ], - where: eq(dbSchema.station.haveSchedule, true), - }) - - if (stations.length === 0) { - logger.error(`[QUERY][STATION][ALL] Stations data is not found`) - throw new Error( - "No station data is existing. Please sync station data first.", - ) - } - - await cache.set(stations) - - return stations - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/station/get-by-id.ts b/src/services/station/get-by-id.ts deleted file mode 100644 index 0929468..0000000 --- a/src/services/station/get-by-id.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { eq } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { db, dbSchema } from "../../db" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { Station } from "../../db/schema" -import Cache from "../../commons/utils/cache" - -export const getItemById = async (stationId: string) => { - try { - const cache = new Cache(`station-${stationId}`, { - ttl: - 60 * - new Date(Date.now()).getMinutes() * - new Date(Date.now()).getHours(), - }) - - const cached = await cache.get() - - if (cached) return cached - - const station = await db.query.station.findFirst({ - where: eq(dbSchema.station.id, stationId), - }) - - if (!station) { - logger.error(`[QUERY][STATION][${stationId}] Station data is not found`) - return null - } - - await cache.set(station) - - return station - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/station/index.ts b/src/services/station/index.ts deleted file mode 100644 index 11da2d2..0000000 --- a/src/services/station/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NotFoundError } from "elysia" -import { syncWrapper } from "../utils/sync" -import { getAll } from "./get-all" -import { getItemById } from "./get-by-id" -import { sync as syncStation } from "./sync" -import { SyncType } from "../../commons/types" - -export const station = { - sync: async (type: SyncType) => { - const data = await syncWrapper(syncStation, { - item: "station", - type, - })() - - return { - status: 200, - data, - } - }, - getAll: async () => { - const stations = await getAll() - - return { - status: 200, - data: stations, - } - }, - getById: async (id: string) => { - const station = await getItemById(id) - - if (!station) { - throw new NotFoundError("Station data is not found") - } - - return { - status: 200, - data: station, - } - }, -} diff --git a/src/services/station/sync.ts b/src/services/station/sync.ts deleted file mode 100644 index 6ec2921..0000000 --- a/src/services/station/sync.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { z } from "zod" -import { db, dbSchema } from "../../db" -import { logger } from "../../commons/utils/log" -import { sql } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { handleError } from "../../commons/utils/error" - -export const sync = async () => { - try { - logger.info("[SYNC][STATION] Syncing station data started") - - const req = await fetch( - "https://api-partner.krl.co.id/krlweb/v1/krl-station", - ).then((res) => res.json()) - - logger.info("[SYNC][STATION] Fetched data from API") - - const schema = z.object({ - status: z.number(), - message: z.string(), - data: z.array( - z.object({ - sta_id: z.string(), - sta_name: z.string(), - group_wil: z.number(), - fg_enable: z.number(), - }), - ), - }) - - const parsed = schema.parse(req) - - const filterdStation = parsed.data.filter((d) => !d.sta_id.includes("WIL")) - - const insert = await db - .insert(dbSchema.station) - .values( - filterdStation.map((s) => { - return { - id: s.sta_id, - name: s.sta_name, - fgEnable: s.fg_enable, - daop: s.group_wil === 0 ? 1 : s.group_wil, - } - }), - ) - .onConflictDoUpdate({ - target: dbSchema.station.id, - set: { - updatedAt: new Date().toISOString(), - id: sql`excluded.id`, - name: sql`excluded.name`, - daop: sql`excluded.daop`, - }, - }) - .returning() - - logger.info(`[SYNC][STATION] Inserted ${insert.length} rows`) - logger.info("[SYNC][STATION] Syncing station data finished") - } catch (e) { - logger.error("[SYNC][STATION] Error", e) - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/sync/get-all.ts b/src/services/sync/get-all.ts deleted file mode 100644 index 218c991..0000000 --- a/src/services/sync/get-all.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { InternalServerError } from "elysia" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" -import { db, dbSchema } from "../../db" -import { asc, desc } from "drizzle-orm" - -export const getAll = async () => { - try { - const items = await db.query.sync.findMany({ - limit: 20, - orderBy: [desc(dbSchema.sync.n), asc(dbSchema.sync.createdAt)], - }) - - if (items.length === 0) { - logger.error(`[QUERY][SYNC][ALL] Sync data is not found`) - return [] - } - - return items - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/sync/get-by-id.ts b/src/services/sync/get-by-id.ts deleted file mode 100644 index 8e6f5b4..0000000 --- a/src/services/sync/get-by-id.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { eq } from "drizzle-orm" -import { InternalServerError } from "elysia" -import { db, dbSchema } from "../../db" -import { handleError } from "../../commons/utils/error" -import { logger } from "../../commons/utils/log" - -export const getItemById = async (syncId: string) => { - try { - const item = await db.query.sync.findFirst({ - where: eq(dbSchema.sync.id, syncId), - }) - - if (!item) { - logger.error(`[QUERY][SYNC][${syncId}] Sync data is not found`) - return null - } - - return item - } catch (e) { - throw new InternalServerError(handleError(e)) - } -} diff --git a/src/services/sync/index.ts b/src/services/sync/index.ts deleted file mode 100644 index eaf722e..0000000 --- a/src/services/sync/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getItemById } from "./get-by-id" -import { getAll } from "./get-all" -import { NotFoundError } from "elysia" - -export const sync = { - getAll: async () => { - const items = await getAll() - - return { - status: 200, - data: items, - } - }, - getItemById: async (id: string) => { - const item = await getItemById(id) - - if (!item) throw new NotFoundError("Sync data is not found") - - return { - status: 200, - data: item, - } - }, -} diff --git a/src/services/utils/sync.ts b/src/services/utils/sync.ts deleted file mode 100644 index d5cb793..0000000 --- a/src/services/utils/sync.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { sql } from "drizzle-orm" -import { db, dbSchema } from "../../db" -import { NewSync, sync } from "../../db/schema" -import { handleError } from "../../commons/utils/error" -import { SyncItem, SyncType } from "../../commons/types" - -/** A function wrapper utils to handle syncing status */ -export const syncWrapper = - ( - fn: () => Promise, - { - item, - type, - }: { - // TODO: Change to infer type from dbSchema.sync - type: SyncType - item: SyncItem - }, - ) => - async () => { - const start = await db - .insert(dbSchema.sync) - .values({ - item, - type, - }) - .returning({ id: dbSchema.sync.id, n: dbSchema.sync.n }) - - const initalPayload = start[0] - const startTime = performance.now() - - fn() - .then(async () => { - const payload: Partial = { - ...initalPayload, - status: "SUCCESS", - } - await db - .insert(dbSchema.sync) - .values(payload) - .onConflictDoUpdate({ - target: dbSchema.sync.id, - set: { - status: sql`excluded.status`, - }, - }) - }) - .catch(async (e) => { - const error = handleError(e) - - const payload: Partial = { - ...initalPayload, - status: "FAILED", - message: error, - } - - await db - .insert(dbSchema.sync) - .values(payload) - .onConflictDoUpdate({ - target: dbSchema.sync.id, - set: { - status: sql`excluded.status`, - message: sql`excluded.message`, - }, - }) - }) - .finally(async () => { - const endTime = performance.now() - const duration = Math.ceil(endTime - startTime) - - const payload: Partial = { - ...initalPayload, - endedAt: new Date().toISOString(), - duration, - } - - await db - .insert(dbSchema.sync) - .values(payload) - .onConflictDoUpdate({ - target: dbSchema.sync.id, - set: { - endedAt: sql`excluded.ended_at`, - duration: sql`excluded.duration`, - }, - }) - }) - - return { - id: initalPayload.id, - type, - item, - status: "PENDING", - } - } diff --git a/src/sync/headers.ts b/src/sync/headers.ts new file mode 100644 index 0000000..a5d570a --- /dev/null +++ b/src/sync/headers.ts @@ -0,0 +1,9 @@ +export const KAI_HEADERS = { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0", + Accept: "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "en-US,en;q=0.5", + Authorization: + "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzIiwianRpIjoiMDYzNWIyOGMzYzg3YTY3ZTRjYWE4YTI0MjYxZGYwYzIxNjYzODA4NWM2NWU4ZjhiYzQ4OGNlM2JiZThmYWNmODU4YzY0YmI0MjgyM2EwOTUiLCJpYXQiOjE3MjI2MTc1MTQsIm5iZiI6MTcyMjYxNzUxNCwiZXhwIjoxNzU0MTUzNTE0LCJzdWIiOiI1Iiwic2NvcGVzIjpbXX0.Jz_sedcMtaZJ4dj0eWVc4_pr_wUQ3s1-UgpopFGhEmJt_iGzj6BdnOEEhcDDdIz-gydQL5ek0S_36v5h6P_X3OQyII3JmHp1SEDJMwrcy4FCY63-jGnhPBb4sprqUFruDRFSEIs1cNQ-3rv3qRDzJtGYc_bAkl2MfgZj85bvt2DDwBWPraZuCCkwz2fJvox-6qz6P7iK9YdQq8AjJfuNdl7t_1hMHixmtDG0KooVnfBV7PoChxvcWvs8FOmtYRdqD7RSEIoOXym2kcwqK-rmbWf9VuPQCN5gjLPimL4t2TbifBg5RWNIAAuHLcYzea48i3okbhkqGGlYTk3iVMU6Hf_Jruns1WJr3A961bd4rny62lNXyGPgNLRJJKedCs5lmtUTr4gZRec4Pz_MqDzlEYC3QzRAOZv0Ergp8-W1Vrv5gYyYNr-YQNdZ01mc7JH72N2dpU9G00K5kYxlcXDNVh8520-R-MrxYbmiFGVlNF2BzEH8qq6Ko9m0jT0NiKEOjetwegrbNdNq_oN4KmHvw2sHkGWY06rUeciYJMhBF1JZuRjj3JTwBUBVXcYZMFtwUAoikVByzKuaZZeTo1AtCiSjejSHNdpLxyKk_SFUzog5MOkUN1ktAhFnBFoz6SlWAJBJIS-lHYsdFLSug2YNiaNllkOUsDbYkiDtmPc9XWc", + Priority: "u=0", +} diff --git a/src/sync/schedule.ts b/src/sync/schedule.ts new file mode 100644 index 0000000..3e4584e --- /dev/null +++ b/src/sync/schedule.ts @@ -0,0 +1,200 @@ +import { sleep } from "bun" +import { eq, sql } from "drizzle-orm" +import { z } from "zod" +import { + NewSchedule, + NewStation, + scheduleTable, + stationTable, +} from "../db/schema" +import { Database } from "../modules/v1/database" +import { parseTime } from "../utils/time" +import { KAI_HEADERS } from "./headers" + +const sync = async () => { + if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL env is missing") + if (!process.env.COMULINE_ENV) throw new Error("COMULINE_ENV env is missing") + + const { db } = new Database({ + COMULINE_ENV: process.env.COMULINE_ENV, + DATABASE_URL: process.env.DATABASE_URL, + }) + + const stations = await db + .select({ + id: stationTable.id, + metadata: stationTable.metadata, + name: stationTable.name, + }) + .from(stationTable) + + const batchSizes = 5 + const totalBatches = Math.ceil(stations.length / batchSizes) + + const schema = z.object({ + status: z.number(), + data: z.array( + z.object({ + train_id: z.string(), + ka_name: z.string(), + route_name: z.string(), + dest: z.string(), + time_est: z.string(), + color: z.string(), + dest_time: z.string(), + }), + ), + }) + + for (let i = 0; i < totalBatches; i++) { + const start = i * batchSizes + const end = start + batchSizes + const batch = stations.slice(start, end) + + await Promise.allSettled( + batch.map(async ({ id, metadata }) => { + await sleep(5000) + + const url = `https://api-partner.krl.co.id/krlweb/v1/schedule?stationid=${id}&timefrom=00:00&timeto=23:00` + + console.info(`[SYNC][SCHEDULE][${id}] Send preflight`) + const optionsResponse = await fetch(url, { + method: "OPTIONS", + headers: { + ...KAI_HEADERS, + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "authorization,content-type", + }, + credentials: "include", + mode: "cors", + }) + + if (!optionsResponse.ok) { + throw new Error( + `OPTIONS request failed with status: ${optionsResponse.status}`, + ) + } + const req = await fetch(url, { + method: "GET", + headers: KAI_HEADERS, + credentials: "include", + mode: "cors", + }) + + console.info(`[SYNC][SCHEDULE][${id}] Fetched data from API`) + + if (req.status === 200) { + try { + const data = await req.json() + + const parsed = schema.safeParse(data) + + if (!parsed.success) { + console.error(`[SYNC][SCHEDULE][${id}] Error parse`) + } else { + const values = parsed.data.data.map((d) => { + let [origin, destination] = d.route_name.split("-") + + const fixName = (name: string) => { + switch (name) { + case "TANJUNGPRIUK": + return "TANJUNG PRIOK" + case "JAKARTAKOTA": + return "JAKARTA KOTA" + case "KAMPUNGBANDAN": + return "KAMPUNG BANDAN" + case "TANAHABANG": + return "TANAH ABANG" + case "PARUNGPANJANG": + return "PARUNG PANJANG" + case "BANDARASOEKARNOHATTA": + return "BANDARA SOEKARNO HATTA" + default: + return name + } + } + + origin = fixName(origin) + destination = fixName(destination) + + return { + id: `sc_krl_${id}_${d.train_id}`.toLowerCase(), + station_id: id, + station_origin_id: stations.find( + ({ name }) => name === origin, + )?.id!, + station_destination_id: stations.find( + ({ name }) => name === destination, + )?.id!, + train_id: d.train_id, + line: d.ka_name, + route: d.route_name, + departs_at: parseTime(d.time_est).toISOString(), + arrives_at: parseTime(d.dest_time).toISOString(), + metadata: { + origin: { + color: d.color, + }, + }, + } satisfies NewSchedule + }) + + const insert = await db + .insert(scheduleTable) + .values(values) + .onConflictDoUpdate({ + target: scheduleTable.id, + set: { + departs_at: sql`excluded.departs_at`, + arrives_at: sql`excluded.arrives_at`, + metadata: sql`excluded.metadata`, + updated_at: new Date().toISOString(), + }, + }) + .returning() + + console.info( + `[SYNC][SCHEDULE][${id}] Inserted ${insert.length} rows`, + ) + } + } catch (err) { + console.error( + `[SYNC][SCHEDULE][${id}] Error inserting schedule data. Trace: ${JSON.stringify( + err, + )}. Status: ${req.status}.`, + ) + } + } else if (req.status === 404) { + console.info(`[SYNC][SCHEDULE][${id}] No schedule data found`) + const payload: Partial = { + metadata: metadata + ? { + ...metadata, + active: false, + } + : null, + updated_at: new Date().toISOString(), + } + await db + .update(stationTable) + .set(payload) + .where(eq(scheduleTable.id, id)) + console.info( + `[SYNC][SCHEDULE][${id}] Updated station schedule availability status`, + ) + } else { + const err = await req.json() + const txt = await req.text() + console.error( + `[SYNC][SCHEDULE][${id}] Error fetch schedule data. Trace: ${JSON.stringify( + err, + )}. Status: ${req.status}. Req: ${txt}`, + ) + throw new Error(JSON.stringify(err)) + } + }), + ) + } +} + +sync() diff --git a/src/sync/station.ts b/src/sync/station.ts new file mode 100644 index 0000000..3a1cc25 --- /dev/null +++ b/src/sync/station.ts @@ -0,0 +1,143 @@ +import { sql } from "drizzle-orm" +import { z } from "zod" +import { NewStation, stationTable, StationType } from "../db/schema" +import { Database } from "../modules/v1/database" +import { KAI_HEADERS } from "./headers" + +const createStationKey = (type: StationType, id: string) => + `st_${type}_${id}`.toLocaleLowerCase() + +const sync = async () => { + if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL env is missing") + if (!process.env.COMULINE_ENV) throw new Error("COMULINE_ENV env is missing") + + const { db } = new Database({ + COMULINE_ENV: process.env.COMULINE_ENV, + DATABASE_URL: process.env.DATABASE_URL, + }) + + const schema = z.object({ + status: z.number(), + message: z.string(), + data: z.array( + z.object({ + sta_id: z.string(), + sta_name: z.string(), + group_wil: z.number(), + fg_enable: z.number(), + }), + ), + }) + + const url = "https://api-partner.krl.co.id/krlweb/v1/krl-station" + + const req = await fetch(url, { + method: "GET", + headers: KAI_HEADERS, + }) + + if (!req.ok) + throw new Error( + `[SYNC][STATION] Request failed with status: ${req.status}`, + { + cause: await req.text(), + }, + ) + + const data = await req.json() + + const parsedData = schema.safeParse(data) + + if (!parsedData.success) { + throw new Error(parsedData.error.message, { + cause: parsedData.error.cause, + }) + } + + const filteredStation = parsedData.data.data.filter( + (d) => !d.sta_id.includes("WIL"), + ) + + const stations = filteredStation.map((s) => { + return { + uid: createStationKey("KRL", s.sta_id), + id: s.sta_id, + name: s.sta_name, + type: "KRL", + metadata: { + active: true, + origin: { + fg_enable: s.fg_enable, + daop: s.group_wil === 0 ? 1 : s.group_wil, + }, + }, + } + }) satisfies NewStation[] + + const newStations = [ + /** Bandara Soekarno Hatta */ + { + uid: createStationKey("KRL", "BST"), + id: "BST", + name: "BANDARA SOEKARNO HATTA", + type: "KRL", + metadata: { + active: true, + origin: { + fg_enable: 1, + daop: 1, + }, + }, + }, + /** Cikampek */ + { + uid: createStationKey("KRL", "CKP"), + id: "CKP", + name: "CIKAMPEK", + type: "LOCAL", + metadata: { + active: true, + origin: { + fg_enable: 1, + daop: 1, + }, + }, + }, + /** Purwakarta */ + { + uid: createStationKey("KRL", "PWK"), + id: "PWK", + name: "PURWAKARTA", + type: "LOCAL", + metadata: { + active: true, + origin: { + fg_enable: 1, + daop: 2, + }, + }, + }, + ] satisfies NewStation[] + + const insertStations = [...newStations, ...stations] + + await db + .insert(stationTable) + .values(insertStations) + .onConflictDoUpdate({ + target: stationTable.uid, + set: { + updated_at: new Date().toISOString(), + uid: sql`excluded.uid`, + id: sql`excluded.id`, + name: sql`excluded.name`, + }, + }) + .returning() + + console.info(`[SYNC][STATION] Inserted ${insertStations.length} rows`) + + process.exit(0) +} + +sync() diff --git a/src/type.ts b/src/type.ts new file mode 100644 index 0000000..94c8a50 --- /dev/null +++ b/src/type.ts @@ -0,0 +1,19 @@ +import { Database } from "./modules/v1/database" +import { constructResponse } from "./utils/response" + +export type Bindings = { + DATABASE_URL: string + COMULINE_ENV: string + UPSTASH_REDIS_REST_TOKEN: string + UPSTASH_REDIS_REST_URL: string +} + +export type Variables = { + db: Database["db"] + constructResponse: typeof constructResponse +} + +export type Environments = { + Bindings: Bindings + Variables: Variables +} diff --git a/src/utils/response.ts b/src/utils/response.ts new file mode 100644 index 0000000..fe920bc --- /dev/null +++ b/src/utils/response.ts @@ -0,0 +1,90 @@ +import { type RouteConfig } from "@hono/zod-openapi" +import { HTTPException } from "hono/http-exception" +import { type StatusCode } from "hono/utils/http-status" +import { z } from "zod" + +export const constructResponse = ( + schema: T, + data: z.infer, +): z.infer => { + const result = schema.safeParse(data) + + if (!result.success) { + console.log(result.error.issues) + throw new HTTPException(417, { + message: "Failed to construct a response", + cause: result.error.issues, + }) + } + + return result.data +} + +interface BaseResponseSchema { + status: number +} + +interface DataResponseSchema extends BaseResponseSchema { + type: "data" + schema: z.ZodTypeAny +} + +interface MetadataResponseSchema extends BaseResponseSchema { + type: "metadata" + description?: string +} + +export const buildResponseSchemas = ( + responses: Array, +): RouteConfig["responses"] => { + let result: RouteConfig["responses"] = {} + + for (const { status, ...rest } of responses) { + if (rest.type === "data") { + const { schema } = rest + result[status] = { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + success: z.boolean().default(true), + }), + data: schema, + }), + }, + }, + description: "Success", + } satisfies RouteConfig["responses"][string] + } else { + const { description } = rest + + const defaultDescription = + description ?? getDefaultDescription(status as StatusCode) + + result[status] = { + content: { + "application/json": { + schema: z.object({ + metadata: z.object({ + success: z.boolean().default(false), + message: z.string().min(1).default(defaultDescription), + }), + }), + }, + }, + description: defaultDescription, + } satisfies RouteConfig["responses"][string] + } + } + + return result +} + +const getDefaultDescription = (status: StatusCode) => { + switch (status) { + case 404: + return "Not found" + default: + return "Internal server error" + } +} diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..9958a33 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,31 @@ +export function getSecsToMidnight(): number { + const now = new Date() + const tomorrow = new Date(now) + tomorrow.setHours(0, 0, 0, 0) + tomorrow.setDate(tomorrow.getDate() + 1) + + return Math.floor((tomorrow.getTime() - now.getTime()) / 1000) +} + +export function parseTime(timeString: string): Date { + const [hours, minutes, seconds] = timeString.split(":").map(Number) + + // Create date object + const date = new Date() + + // Get the timezone offset in minutes (GMT+7 = -420 minutes) + const targetOffset = -420 // GMT+7 in minutes + const currentOffset = date.getTimezoneOffset() + + // Calculate the difference in offset + const offsetDiff = targetOffset - currentOffset + + // Set time components and adjust for timezone + date.setHours( + hours ?? date.getHours(), + (minutes ?? date.getMinutes()) + offsetDiff, + seconds ?? date.getSeconds(), + ) + + return date +} diff --git a/tsconfig.json b/tsconfig.json index 2ca47bb..9c7a8eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -100,6 +100,10 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "baseUrl": "./src", + "paths": { + "@/*": ["*"] + } } } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..3c31d34 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig, type Options } from "tsup" + +export default defineConfig((options: Options) => ({ + entry: ["src/index.ts"], + format: ["esm"], + minify: true, + outDir: ".dist", + clean: true, + metafile: true, + ...options, +})) diff --git a/wrangler.example.toml b/wrangler.example.toml new file mode 100644 index 0000000..f227d3b --- /dev/null +++ b/wrangler.example.toml @@ -0,0 +1,10 @@ +name = "comuline-api" +compatibility_date = "2024-08-21" +main = "src/index.ts" +minify = true + +[limits] +cpu_ms = 30000 + + +