├── .dev.example.vars ├── .dockerignore ├── .env.db ├── .github └── workflows │ ├── api.build.yml │ └── api.deploy.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc.js ├── README.md ├── bun.lockb ├── docker-compose.yml ├── drizzle.config.ts ├── drizzle └── migrations │ ├── 0000_talented_daimon_hellstrom.sql │ ├── 0001_tiny_shadowcat.sql │ ├── 0002_serious_the_hand.sql │ ├── 0003_first_dorian_gray.sql │ ├── 0004_great_vance_astro.sql │ ├── 0005_rare_senator_kelly.sql │ ├── 0006_hesitant_hedge_knight.sql │ ├── 0007_naive_pepper_potts.sql │ ├── 0008_wooden_purple_man.sql │ └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ ├── 0004_snapshot.json │ ├── 0005_snapshot.json │ ├── 0006_snapshot.json │ ├── 0007_snapshot.json │ ├── 0008_snapshot.json │ └── _journal.json ├── package.json ├── src ├── db │ ├── migrate.ts │ └── schema │ │ ├── index.ts │ │ ├── schedule.table.ts │ │ └── station.table.ts ├── index.ts ├── modules │ ├── api.ts │ └── v1 │ │ ├── cache.ts │ │ ├── database.ts │ │ ├── index.ts │ │ ├── route │ │ ├── route.controller.ts │ │ └── route.schema.ts │ │ ├── schedule │ │ ├── schedule.controller.ts │ │ └── schedule.schema.ts │ │ └── station │ │ ├── station.controller.ts │ │ └── station.schema.ts ├── sync │ ├── headers.ts │ ├── schedule.ts │ └── station.ts ├── type.ts └── utils │ ├── response.ts │ └── time.ts ├── tsconfig.json ├── tsup.config.ts └── wrangler.jsonc /.dev.example.vars: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://comuline:password@localhost:5432/comuline" 2 | COMULINE_ENV="development" 3 | 4 | # Take token from .env.db 5 | UPSTASH_REDIS_REST_TOKEN="" 6 | UPSTASH_REDIS_REST_URL="http://localhost:8079" 7 | 8 | # KRL stuff 9 | KRL_ENDPOINT_BASE_URL="https://api-partner.krl.co.id/krl-webs/v1" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | .pnp 7 | **/.pnp.js 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next 14 | out 15 | 16 | # production 17 | build 18 | 19 | # misc 20 | **/.DS_Store 21 | **/*.pem 22 | 23 | # debug 24 | **/npm-debug.log* 25 | **/yarn-debug.log* 26 | **/yarn-error.log* 27 | 28 | # local env files 29 | **/.env.local 30 | **/.env.development.local 31 | **/.env.test.local 32 | **/.env.production.local 33 | 34 | # vercel 35 | **/.vercel 36 | 37 | **/**/*.trace 38 | **/**/*.zip 39 | **/**/*.tar.gz 40 | **/**/*.tgz 41 | **/**/*.log 42 | **/package-lock.json 43 | **/**/*.bun 44 | fly.toml -------------------------------------------------------------------------------- /.env.db: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | POSTGRES_USER="comuline" 3 | POSTGRES_PASSWORD="password" 4 | POSTGRES_DB="comuline" 5 | 6 | # Redis 7 | SRH_MODE=env 8 | # openssl rand -base64 32 9 | SRH_TOKEN="" 10 | SRH_CONNECTION_STRING=redis://redis:6379 -------------------------------------------------------------------------------- /.github/workflows/api.build.yml: -------------------------------------------------------------------------------- 1 | name: Build API on Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | types: [opened, reopened, synchronize] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Bun 20 | uses: oven-sh/setup-bun@v2 21 | with: 22 | bun-version: "latest" 23 | 24 | - name: Install package deps 25 | run: bun i 26 | 27 | - name: Build package 28 | run: bun run build 29 | -------------------------------------------------------------------------------- /.github/workflows/api.deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy API to Cloudflare Workers 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Installing Bun 15 | uses: oven-sh/setup-bun@v2 16 | 17 | - name: Install package deps 18 | run: bun i 19 | 20 | - name: Build package 21 | run: bun run build 22 | env: 23 | COMULINE_ENV: production 24 | 25 | - name: Deploy to Cloudflare Workers 26 | uses: cloudflare/wrangler-action@v3 27 | with: 28 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 29 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 30 | command: deploy -c wrangler.jsonc 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | **/*.trace 37 | **/*.zip 38 | **/*.tar.gz 39 | **/*.tgz 40 | **/*.log 41 | package-lock.json 42 | **/*.bun 43 | .env 44 | .wrangler 45 | .dev.vars 46 | 47 | # build 48 | .dist -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun run lint-staged -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "always", 3 | singleQuote: false, 4 | jsxSingleQuote: false, 5 | tabWidth: 2, 6 | semi: false, 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @comuline/api 2 | 3 | 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)). 4 | 5 | ### How does it work? 6 | 7 | This API uses a daily cron job (at 00:00) to fetch the schedule of KRL commuter line in Jakarta and Yogyakarta from the official website of PT. KAI. The data is then processed and stored in a PostgreSQL database and cached in a Redis (for every once read request). All endpoints can be found in the [docs](https://www.api.comuline.com/docs). 8 | 9 | ### Technology stacks 10 | 11 | 1. [Hono](https://hono.dev/) API framework 12 | 2. [Bun](https://bun.sh/) runtime 13 | 3. (Serverless) PostgresSQL ([Neon](https://neon.tech/)) 14 | 4. (Serverless) Redis ([Upstash](https://upstash.com/)) 15 | 5. [Cloudflare Workers](https://workers.cloudflare.com/) deployment platform 16 | 6. [Drizzle](https://orm.drizzle.team/) ORM 17 | 18 | ## Getting Started 19 | 20 | ### Development 21 | 22 | 1. Clone the repository 23 | 24 | ```bash 25 | git clone https://github.com/comuline/api.git 26 | ``` 27 | 28 | 2. Install the dependencies 29 | 30 | ```bash 31 | bun install 32 | ``` 33 | 34 | 3. Copy the `.dev.example.vars` to `.dev.vars` 35 | 36 | ``` 37 | cp .dev.example.vars .dev.vars 38 | ``` 39 | 40 | 4. Generate `UPSTASH_REDIS_REST_TOKEN` using `openssl rand -hex 32` and copy it to your `.dev.vars` file 41 | 42 | 5. Run database locally 43 | 44 | ```bash 45 | docker-compose up -d 46 | ``` 47 | 48 | 6. Run the database migration 49 | 50 | ```bash 51 | bun run migrate:apply 52 | ``` 53 | 54 | 7. Sync the data and populate it into your local database (once only as you needed) 55 | 56 | ```bash 57 | # Please do this in order 58 | # 1. Sync station data and wait until it's done 59 | bun run sync:station 60 | # 2. Sync schedule data 61 | bun run sync:schedule 62 | ``` 63 | 64 | ### Deployment 65 | 66 | 1. Rename the `wrand.example.toml` to `wrangler.toml` and fill the necessary information 67 | 68 | 2. Create a new PostgreSQL database in [Neon](https://neon.tech/) and copy the connection string value as `DATABASE_URL` in your `.production.vars` file 69 | 70 | 3. Run the database migration 71 | 72 | ```bash 73 | bun run migrate:apply 74 | ``` 75 | 76 | 4. Sync the data and populate it into your remote database (once only as you needed) 77 | 78 | ```bash 79 | # Please do this in order 80 | # 1. Sync station data and wait until it's done 81 | bun run sync:station 82 | # 2. Sync schedule data 83 | bun run sync:schedule 84 | ``` 85 | 86 | 6. Add `COMULINE_ENV` to your `.production.vars` file 87 | 88 | ``` 89 | COMULINE_ENV=production 90 | ``` 91 | 92 | 6. 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 93 | 94 | 7. Save your `.production.vars` file to your environment variables in your Cloudflare Workers using `wrangler` 95 | 96 | ```bash 97 | bunx wrangler secret put --env production $(cat .production.vars) 98 | ``` 99 | 100 | 8. Deploy the API to Cloudflare Workers 101 | 102 | ```bash 103 | bun run deploy 104 | ``` 105 | 106 | ### Database schema 107 | 108 | > TBD 109 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comuline/api/ca6f7656d69a25e511fea3e437070cf53846d5a1/bun.lockb -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | # Serverless PostgresSQL 5 | postgres: 6 | image: "postgres:15.2-alpine" 7 | ports: 8 | - "5432:5432" 9 | env_file: 10 | - ./.env.db 11 | pg_proxy: 12 | image: ghcr.io/neondatabase/wsproxy:latest 13 | environment: 14 | APPEND_PORT: "postgres:5432" 15 | ALLOW_ADDR_REGEX: ".*" 16 | LOG_TRAFFIC: "true" 17 | ports: 18 | - "5433:80" 19 | depends_on: 20 | - postgres 21 | 22 | # Serverless Redis 23 | redis: 24 | image: redis 25 | ports: 26 | - "6379:6379" 27 | serverless-redis-http: 28 | ports: 29 | - "8079:80" 30 | image: hiett/serverless-redis-http:latest 31 | env_file: 32 | - ./.env.db 33 | depends_on: 34 | - redis 35 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit" 2 | 3 | export default { 4 | out: "./drizzle/migrations", 5 | dialect: "postgresql", 6 | schema: "./src/db/schema", 7 | } satisfies Config 8 | -------------------------------------------------------------------------------- /drizzle/migrations/0000_talented_daimon_hellstrom.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | CREATE TYPE "public"."station_type" AS ENUM('KRL', 'MRT', 'LRT'); 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | --> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "schedule" ( 8 | "id" text PRIMARY KEY NOT NULL, 9 | "station_id" text NOT NULL, 10 | "station_origin_id" text, 11 | "station_destination_id" text, 12 | "train_id" text NOT NULL, 13 | "line" text NOT NULL, 14 | "route" text NOT NULL, 15 | "time_departure" time NOT NULL, 16 | "time_at_destination" time NOT NULL, 17 | "metadata" jsonb, 18 | "created_at" timestamp with time zone DEFAULT now(), 19 | "updated_at" timestamp with time zone DEFAULT now(), 20 | CONSTRAINT "schedule_id_unique" UNIQUE("id") 21 | ); 22 | --> statement-breakpoint 23 | CREATE TABLE IF NOT EXISTS "station" ( 24 | "uid" text PRIMARY KEY NOT NULL, 25 | "id" text NOT NULL, 26 | "name" text NOT NULL, 27 | "type" "station_type" NOT NULL, 28 | "metadata" jsonb, 29 | "created_at" timestamp with time zone DEFAULT now(), 30 | "updated_at" timestamp with time zone DEFAULT now(), 31 | CONSTRAINT "station_uid_unique" UNIQUE("uid"), 32 | CONSTRAINT "station_id_unique" UNIQUE("id") 33 | ); 34 | --> statement-breakpoint 35 | DO $$ BEGIN 36 | 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; 37 | EXCEPTION 38 | WHEN duplicate_object THEN null; 39 | END $$; 40 | --> statement-breakpoint 41 | DO $$ BEGIN 42 | 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; 43 | EXCEPTION 44 | WHEN duplicate_object THEN null; 45 | END $$; 46 | --> statement-breakpoint 47 | DO $$ BEGIN 48 | 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; 49 | EXCEPTION 50 | WHEN duplicate_object THEN null; 51 | END $$; 52 | --> statement-breakpoint 53 | CREATE UNIQUE INDEX IF NOT EXISTS "schedule_idx" ON "schedule" USING btree ("id");--> statement-breakpoint 54 | CREATE INDEX IF NOT EXISTS "schedule_station_idx" ON "schedule" USING btree ("station_id");--> statement-breakpoint 55 | CREATE UNIQUE INDEX IF NOT EXISTS "station_uidx" ON "station" USING btree ("uid");--> statement-breakpoint 56 | CREATE INDEX IF NOT EXISTS "station_idx" ON "station" USING btree ("id");--> statement-breakpoint 57 | CREATE INDEX IF NOT EXISTS "station_type_idx" ON "station" USING btree ("type"); -------------------------------------------------------------------------------- /drizzle/migrations/0001_tiny_shadowcat.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE "station_type" ADD VALUE 'LOCAL'; -------------------------------------------------------------------------------- /drizzle/migrations/0002_serious_the_hand.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_id_station_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_origin_id_station_id_fk"; 4 | --> statement-breakpoint 5 | ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_destination_id_station_id_fk"; 6 | --> statement-breakpoint 7 | DO $$ BEGIN 8 | 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; 9 | EXCEPTION 10 | WHEN duplicate_object THEN null; 11 | END $$; 12 | --> statement-breakpoint 13 | DO $$ BEGIN 14 | 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; 15 | EXCEPTION 16 | WHEN duplicate_object THEN null; 17 | END $$; 18 | --> statement-breakpoint 19 | DO $$ BEGIN 20 | 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; 21 | EXCEPTION 22 | WHEN duplicate_object THEN null; 23 | END $$; 24 | -------------------------------------------------------------------------------- /drizzle/migrations/0003_first_dorian_gray.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "schedule_train_idx" ON "schedule" USING btree ("train_id"); -------------------------------------------------------------------------------- /drizzle/migrations/0004_great_vance_astro.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "schedule" ADD COLUMN "departs_at" timestamp with time zone DEFAULT now();--> statement-breakpoint 2 | ALTER TABLE "schedule" ADD COLUMN "arrives_at" timestamp with time zone DEFAULT now();--> statement-breakpoint 3 | ALTER TABLE "schedule" DROP COLUMN IF EXISTS "time_departure";--> statement-breakpoint 4 | ALTER TABLE "schedule" DROP COLUMN IF EXISTS "time_at_destination"; -------------------------------------------------------------------------------- /drizzle/migrations/0005_rare_senator_kelly.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "schedule" ALTER COLUMN "departs_at" SET NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "schedule" ALTER COLUMN "arrives_at" SET NOT NULL;--> statement-breakpoint 3 | ALTER TABLE "schedule" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint 4 | ALTER TABLE "schedule" ALTER COLUMN "updated_at" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/migrations/0006_hesitant_hedge_knight.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "station" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "station" ALTER COLUMN "updated_at" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/migrations/0007_naive_pepper_potts.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "schedule" ALTER COLUMN "station_origin_id" SET NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "schedule" ALTER COLUMN "station_destination_id" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/migrations/0008_wooden_purple_man.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "schedule" DROP CONSTRAINT "schedule_station_origin_id_station_id_fk"; 2 | --> statement-breakpoint 3 | DO $$ BEGIN 4 | ALTER TABLE "schedule" ADD CONSTRAINT "schedule_station_origin_id_station_id_fk" FOREIGN KEY ("station_origin_id") REFERENCES "public"."station"("id") ON DELETE cascade ON UPDATE no action; 5 | EXCEPTION 6 | WHEN duplicate_object THEN null; 7 | END $$; 8 | -------------------------------------------------------------------------------- /drizzle/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ebac93b2-10b0-46c7-8348-9827ee12aef5", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.schedule": { 8 | "name": "schedule", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "station_id": { 18 | "name": "station_id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "station_origin_id": { 24 | "name": "station_origin_id", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": false 28 | }, 29 | "station_destination_id": { 30 | "name": "station_destination_id", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "train_id": { 36 | "name": "train_id", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": true 40 | }, 41 | "line": { 42 | "name": "line", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": true 46 | }, 47 | "route": { 48 | "name": "route", 49 | "type": "text", 50 | "primaryKey": false, 51 | "notNull": true 52 | }, 53 | "time_departure": { 54 | "name": "time_departure", 55 | "type": "time", 56 | "primaryKey": false, 57 | "notNull": true 58 | }, 59 | "time_at_destination": { 60 | "name": "time_at_destination", 61 | "type": "time", 62 | "primaryKey": false, 63 | "notNull": true 64 | }, 65 | "metadata": { 66 | "name": "metadata", 67 | "type": "jsonb", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "created_at": { 72 | "name": "created_at", 73 | "type": "timestamp with time zone", 74 | "primaryKey": false, 75 | "notNull": false, 76 | "default": "now()" 77 | }, 78 | "updated_at": { 79 | "name": "updated_at", 80 | "type": "timestamp with time zone", 81 | "primaryKey": false, 82 | "notNull": false, 83 | "default": "now()" 84 | } 85 | }, 86 | "indexes": { 87 | "schedule_idx": { 88 | "name": "schedule_idx", 89 | "columns": [ 90 | { 91 | "expression": "id", 92 | "isExpression": false, 93 | "asc": true, 94 | "nulls": "last" 95 | } 96 | ], 97 | "isUnique": true, 98 | "concurrently": false, 99 | "method": "btree", 100 | "with": {} 101 | }, 102 | "schedule_station_idx": { 103 | "name": "schedule_station_idx", 104 | "columns": [ 105 | { 106 | "expression": "station_id", 107 | "isExpression": false, 108 | "asc": true, 109 | "nulls": "last" 110 | } 111 | ], 112 | "isUnique": false, 113 | "concurrently": false, 114 | "method": "btree", 115 | "with": {} 116 | } 117 | }, 118 | "foreignKeys": { 119 | "schedule_station_id_station_id_fk": { 120 | "name": "schedule_station_id_station_id_fk", 121 | "tableFrom": "schedule", 122 | "tableTo": "station", 123 | "columnsFrom": [ 124 | "station_id" 125 | ], 126 | "columnsTo": [ 127 | "id" 128 | ], 129 | "onDelete": "no action", 130 | "onUpdate": "no action" 131 | }, 132 | "schedule_station_origin_id_station_id_fk": { 133 | "name": "schedule_station_origin_id_station_id_fk", 134 | "tableFrom": "schedule", 135 | "tableTo": "station", 136 | "columnsFrom": [ 137 | "station_origin_id" 138 | ], 139 | "columnsTo": [ 140 | "id" 141 | ], 142 | "onDelete": "no action", 143 | "onUpdate": "no action" 144 | }, 145 | "schedule_station_destination_id_station_id_fk": { 146 | "name": "schedule_station_destination_id_station_id_fk", 147 | "tableFrom": "schedule", 148 | "tableTo": "station", 149 | "columnsFrom": [ 150 | "station_destination_id" 151 | ], 152 | "columnsTo": [ 153 | "id" 154 | ], 155 | "onDelete": "no action", 156 | "onUpdate": "no action" 157 | } 158 | }, 159 | "compositePrimaryKeys": {}, 160 | "uniqueConstraints": { 161 | "schedule_id_unique": { 162 | "name": "schedule_id_unique", 163 | "nullsNotDistinct": false, 164 | "columns": [ 165 | "id" 166 | ] 167 | } 168 | } 169 | }, 170 | "public.station": { 171 | "name": "station", 172 | "schema": "", 173 | "columns": { 174 | "uid": { 175 | "name": "uid", 176 | "type": "text", 177 | "primaryKey": true, 178 | "notNull": true 179 | }, 180 | "id": { 181 | "name": "id", 182 | "type": "text", 183 | "primaryKey": false, 184 | "notNull": true 185 | }, 186 | "name": { 187 | "name": "name", 188 | "type": "text", 189 | "primaryKey": false, 190 | "notNull": true 191 | }, 192 | "type": { 193 | "name": "type", 194 | "type": "station_type", 195 | "typeSchema": "public", 196 | "primaryKey": false, 197 | "notNull": true 198 | }, 199 | "metadata": { 200 | "name": "metadata", 201 | "type": "jsonb", 202 | "primaryKey": false, 203 | "notNull": false 204 | }, 205 | "created_at": { 206 | "name": "created_at", 207 | "type": "timestamp with time zone", 208 | "primaryKey": false, 209 | "notNull": false, 210 | "default": "now()" 211 | }, 212 | "updated_at": { 213 | "name": "updated_at", 214 | "type": "timestamp with time zone", 215 | "primaryKey": false, 216 | "notNull": false, 217 | "default": "now()" 218 | } 219 | }, 220 | "indexes": { 221 | "station_uidx": { 222 | "name": "station_uidx", 223 | "columns": [ 224 | { 225 | "expression": "uid", 226 | "isExpression": false, 227 | "asc": true, 228 | "nulls": "last" 229 | } 230 | ], 231 | "isUnique": true, 232 | "concurrently": false, 233 | "method": "btree", 234 | "with": {} 235 | }, 236 | "station_idx": { 237 | "name": "station_idx", 238 | "columns": [ 239 | { 240 | "expression": "id", 241 | "isExpression": false, 242 | "asc": true, 243 | "nulls": "last" 244 | } 245 | ], 246 | "isUnique": false, 247 | "concurrently": false, 248 | "method": "btree", 249 | "with": {} 250 | }, 251 | "station_type_idx": { 252 | "name": "station_type_idx", 253 | "columns": [ 254 | { 255 | "expression": "type", 256 | "isExpression": false, 257 | "asc": true, 258 | "nulls": "last" 259 | } 260 | ], 261 | "isUnique": false, 262 | "concurrently": false, 263 | "method": "btree", 264 | "with": {} 265 | } 266 | }, 267 | "foreignKeys": {}, 268 | "compositePrimaryKeys": {}, 269 | "uniqueConstraints": { 270 | "station_uid_unique": { 271 | "name": "station_uid_unique", 272 | "nullsNotDistinct": false, 273 | "columns": [ 274 | "uid" 275 | ] 276 | }, 277 | "station_id_unique": { 278 | "name": "station_id_unique", 279 | "nullsNotDistinct": false, 280 | "columns": [ 281 | "id" 282 | ] 283 | } 284 | } 285 | } 286 | }, 287 | "enums": { 288 | "public.station_type": { 289 | "name": "station_type", 290 | "schema": "public", 291 | "values": [ 292 | "KRL", 293 | "MRT", 294 | "LRT" 295 | ] 296 | } 297 | }, 298 | "schemas": {}, 299 | "sequences": {}, 300 | "_meta": { 301 | "columns": {}, 302 | "schemas": {}, 303 | "tables": {} 304 | } 305 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dd1edb26-6f6a-4a0b-856b-8b73207f9055", 3 | "prevId": "ebac93b2-10b0-46c7-8348-9827ee12aef5", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.schedule": { 8 | "name": "schedule", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "station_id": { 18 | "name": "station_id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "station_origin_id": { 24 | "name": "station_origin_id", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": false 28 | }, 29 | "station_destination_id": { 30 | "name": "station_destination_id", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "train_id": { 36 | "name": "train_id", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": true 40 | }, 41 | "line": { 42 | "name": "line", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": true 46 | }, 47 | "route": { 48 | "name": "route", 49 | "type": "text", 50 | "primaryKey": false, 51 | "notNull": true 52 | }, 53 | "time_departure": { 54 | "name": "time_departure", 55 | "type": "time", 56 | "primaryKey": false, 57 | "notNull": true 58 | }, 59 | "time_at_destination": { 60 | "name": "time_at_destination", 61 | "type": "time", 62 | "primaryKey": false, 63 | "notNull": true 64 | }, 65 | "metadata": { 66 | "name": "metadata", 67 | "type": "jsonb", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "created_at": { 72 | "name": "created_at", 73 | "type": "timestamp with time zone", 74 | "primaryKey": false, 75 | "notNull": false, 76 | "default": "now()" 77 | }, 78 | "updated_at": { 79 | "name": "updated_at", 80 | "type": "timestamp with time zone", 81 | "primaryKey": false, 82 | "notNull": false, 83 | "default": "now()" 84 | } 85 | }, 86 | "indexes": { 87 | "schedule_idx": { 88 | "name": "schedule_idx", 89 | "columns": [ 90 | { 91 | "expression": "id", 92 | "isExpression": false, 93 | "asc": true, 94 | "nulls": "last" 95 | } 96 | ], 97 | "isUnique": true, 98 | "concurrently": false, 99 | "method": "btree", 100 | "with": {} 101 | }, 102 | "schedule_station_idx": { 103 | "name": "schedule_station_idx", 104 | "columns": [ 105 | { 106 | "expression": "station_id", 107 | "isExpression": false, 108 | "asc": true, 109 | "nulls": "last" 110 | } 111 | ], 112 | "isUnique": false, 113 | "concurrently": false, 114 | "method": "btree", 115 | "with": {} 116 | } 117 | }, 118 | "foreignKeys": { 119 | "schedule_station_id_station_id_fk": { 120 | "name": "schedule_station_id_station_id_fk", 121 | "tableFrom": "schedule", 122 | "tableTo": "station", 123 | "columnsFrom": [ 124 | "station_id" 125 | ], 126 | "columnsTo": [ 127 | "id" 128 | ], 129 | "onDelete": "no action", 130 | "onUpdate": "no action" 131 | }, 132 | "schedule_station_origin_id_station_id_fk": { 133 | "name": "schedule_station_origin_id_station_id_fk", 134 | "tableFrom": "schedule", 135 | "tableTo": "station", 136 | "columnsFrom": [ 137 | "station_origin_id" 138 | ], 139 | "columnsTo": [ 140 | "id" 141 | ], 142 | "onDelete": "no action", 143 | "onUpdate": "no action" 144 | }, 145 | "schedule_station_destination_id_station_id_fk": { 146 | "name": "schedule_station_destination_id_station_id_fk", 147 | "tableFrom": "schedule", 148 | "tableTo": "station", 149 | "columnsFrom": [ 150 | "station_destination_id" 151 | ], 152 | "columnsTo": [ 153 | "id" 154 | ], 155 | "onDelete": "no action", 156 | "onUpdate": "no action" 157 | } 158 | }, 159 | "compositePrimaryKeys": {}, 160 | "uniqueConstraints": { 161 | "schedule_id_unique": { 162 | "name": "schedule_id_unique", 163 | "nullsNotDistinct": false, 164 | "columns": [ 165 | "id" 166 | ] 167 | } 168 | } 169 | }, 170 | "public.station": { 171 | "name": "station", 172 | "schema": "", 173 | "columns": { 174 | "uid": { 175 | "name": "uid", 176 | "type": "text", 177 | "primaryKey": true, 178 | "notNull": true 179 | }, 180 | "id": { 181 | "name": "id", 182 | "type": "text", 183 | "primaryKey": false, 184 | "notNull": true 185 | }, 186 | "name": { 187 | "name": "name", 188 | "type": "text", 189 | "primaryKey": false, 190 | "notNull": true 191 | }, 192 | "type": { 193 | "name": "type", 194 | "type": "station_type", 195 | "typeSchema": "public", 196 | "primaryKey": false, 197 | "notNull": true 198 | }, 199 | "metadata": { 200 | "name": "metadata", 201 | "type": "jsonb", 202 | "primaryKey": false, 203 | "notNull": false 204 | }, 205 | "created_at": { 206 | "name": "created_at", 207 | "type": "timestamp with time zone", 208 | "primaryKey": false, 209 | "notNull": false, 210 | "default": "now()" 211 | }, 212 | "updated_at": { 213 | "name": "updated_at", 214 | "type": "timestamp with time zone", 215 | "primaryKey": false, 216 | "notNull": false, 217 | "default": "now()" 218 | } 219 | }, 220 | "indexes": { 221 | "station_uidx": { 222 | "name": "station_uidx", 223 | "columns": [ 224 | { 225 | "expression": "uid", 226 | "isExpression": false, 227 | "asc": true, 228 | "nulls": "last" 229 | } 230 | ], 231 | "isUnique": true, 232 | "concurrently": false, 233 | "method": "btree", 234 | "with": {} 235 | }, 236 | "station_idx": { 237 | "name": "station_idx", 238 | "columns": [ 239 | { 240 | "expression": "id", 241 | "isExpression": false, 242 | "asc": true, 243 | "nulls": "last" 244 | } 245 | ], 246 | "isUnique": false, 247 | "concurrently": false, 248 | "method": "btree", 249 | "with": {} 250 | }, 251 | "station_type_idx": { 252 | "name": "station_type_idx", 253 | "columns": [ 254 | { 255 | "expression": "type", 256 | "isExpression": false, 257 | "asc": true, 258 | "nulls": "last" 259 | } 260 | ], 261 | "isUnique": false, 262 | "concurrently": false, 263 | "method": "btree", 264 | "with": {} 265 | } 266 | }, 267 | "foreignKeys": {}, 268 | "compositePrimaryKeys": {}, 269 | "uniqueConstraints": { 270 | "station_uid_unique": { 271 | "name": "station_uid_unique", 272 | "nullsNotDistinct": false, 273 | "columns": [ 274 | "uid" 275 | ] 276 | }, 277 | "station_id_unique": { 278 | "name": "station_id_unique", 279 | "nullsNotDistinct": false, 280 | "columns": [ 281 | "id" 282 | ] 283 | } 284 | } 285 | } 286 | }, 287 | "enums": { 288 | "public.station_type": { 289 | "name": "station_type", 290 | "schema": "public", 291 | "values": [ 292 | "KRL", 293 | "MRT", 294 | "LRT", 295 | "LOCAL" 296 | ] 297 | } 298 | }, 299 | "schemas": {}, 300 | "sequences": {}, 301 | "_meta": { 302 | "columns": {}, 303 | "schemas": {}, 304 | "tables": {} 305 | } 306 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5db983ea-4af8-40b5-9bca-1f274a3c4e3a", 3 | "prevId": "dd1edb26-6f6a-4a0b-856b-8b73207f9055", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.schedule": { 8 | "name": "schedule", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "station_id": { 18 | "name": "station_id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "station_origin_id": { 24 | "name": "station_origin_id", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": false 28 | }, 29 | "station_destination_id": { 30 | "name": "station_destination_id", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "train_id": { 36 | "name": "train_id", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": true 40 | }, 41 | "line": { 42 | "name": "line", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": true 46 | }, 47 | "route": { 48 | "name": "route", 49 | "type": "text", 50 | "primaryKey": false, 51 | "notNull": true 52 | }, 53 | "time_departure": { 54 | "name": "time_departure", 55 | "type": "time", 56 | "primaryKey": false, 57 | "notNull": true 58 | }, 59 | "time_at_destination": { 60 | "name": "time_at_destination", 61 | "type": "time", 62 | "primaryKey": false, 63 | "notNull": true 64 | }, 65 | "metadata": { 66 | "name": "metadata", 67 | "type": "jsonb", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "created_at": { 72 | "name": "created_at", 73 | "type": "timestamp with time zone", 74 | "primaryKey": false, 75 | "notNull": false, 76 | "default": "now()" 77 | }, 78 | "updated_at": { 79 | "name": "updated_at", 80 | "type": "timestamp with time zone", 81 | "primaryKey": false, 82 | "notNull": false, 83 | "default": "now()" 84 | } 85 | }, 86 | "indexes": { 87 | "schedule_idx": { 88 | "name": "schedule_idx", 89 | "columns": [ 90 | { 91 | "expression": "id", 92 | "isExpression": false, 93 | "asc": true, 94 | "nulls": "last" 95 | } 96 | ], 97 | "isUnique": true, 98 | "concurrently": false, 99 | "method": "btree", 100 | "with": {} 101 | }, 102 | "schedule_station_idx": { 103 | "name": "schedule_station_idx", 104 | "columns": [ 105 | { 106 | "expression": "station_id", 107 | "isExpression": false, 108 | "asc": true, 109 | "nulls": "last" 110 | } 111 | ], 112 | "isUnique": false, 113 | "concurrently": false, 114 | "method": "btree", 115 | "with": {} 116 | } 117 | }, 118 | "foreignKeys": { 119 | "schedule_station_id_station_id_fk": { 120 | "name": "schedule_station_id_station_id_fk", 121 | "tableFrom": "schedule", 122 | "tableTo": "station", 123 | "columnsFrom": [ 124 | "station_id" 125 | ], 126 | "columnsTo": [ 127 | "id" 128 | ], 129 | "onDelete": "cascade", 130 | "onUpdate": "no action" 131 | }, 132 | "schedule_station_origin_id_station_id_fk": { 133 | "name": "schedule_station_origin_id_station_id_fk", 134 | "tableFrom": "schedule", 135 | "tableTo": "station", 136 | "columnsFrom": [ 137 | "station_origin_id" 138 | ], 139 | "columnsTo": [ 140 | "id" 141 | ], 142 | "onDelete": "set null", 143 | "onUpdate": "no action" 144 | }, 145 | "schedule_station_destination_id_station_id_fk": { 146 | "name": "schedule_station_destination_id_station_id_fk", 147 | "tableFrom": "schedule", 148 | "tableTo": "station", 149 | "columnsFrom": [ 150 | "station_destination_id" 151 | ], 152 | "columnsTo": [ 153 | "id" 154 | ], 155 | "onDelete": "set null", 156 | "onUpdate": "no action" 157 | } 158 | }, 159 | "compositePrimaryKeys": {}, 160 | "uniqueConstraints": { 161 | "schedule_id_unique": { 162 | "name": "schedule_id_unique", 163 | "nullsNotDistinct": false, 164 | "columns": [ 165 | "id" 166 | ] 167 | } 168 | } 169 | }, 170 | "public.station": { 171 | "name": "station", 172 | "schema": "", 173 | "columns": { 174 | "uid": { 175 | "name": "uid", 176 | "type": "text", 177 | "primaryKey": true, 178 | "notNull": true 179 | }, 180 | "id": { 181 | "name": "id", 182 | "type": "text", 183 | "primaryKey": false, 184 | "notNull": true 185 | }, 186 | "name": { 187 | "name": "name", 188 | "type": "text", 189 | "primaryKey": false, 190 | "notNull": true 191 | }, 192 | "type": { 193 | "name": "type", 194 | "type": "station_type", 195 | "typeSchema": "public", 196 | "primaryKey": false, 197 | "notNull": true 198 | }, 199 | "metadata": { 200 | "name": "metadata", 201 | "type": "jsonb", 202 | "primaryKey": false, 203 | "notNull": false 204 | }, 205 | "created_at": { 206 | "name": "created_at", 207 | "type": "timestamp with time zone", 208 | "primaryKey": false, 209 | "notNull": false, 210 | "default": "now()" 211 | }, 212 | "updated_at": { 213 | "name": "updated_at", 214 | "type": "timestamp with time zone", 215 | "primaryKey": false, 216 | "notNull": false, 217 | "default": "now()" 218 | } 219 | }, 220 | "indexes": { 221 | "station_uidx": { 222 | "name": "station_uidx", 223 | "columns": [ 224 | { 225 | "expression": "uid", 226 | "isExpression": false, 227 | "asc": true, 228 | "nulls": "last" 229 | } 230 | ], 231 | "isUnique": true, 232 | "concurrently": false, 233 | "method": "btree", 234 | "with": {} 235 | }, 236 | "station_idx": { 237 | "name": "station_idx", 238 | "columns": [ 239 | { 240 | "expression": "id", 241 | "isExpression": false, 242 | "asc": true, 243 | "nulls": "last" 244 | } 245 | ], 246 | "isUnique": false, 247 | "concurrently": false, 248 | "method": "btree", 249 | "with": {} 250 | }, 251 | "station_type_idx": { 252 | "name": "station_type_idx", 253 | "columns": [ 254 | { 255 | "expression": "type", 256 | "isExpression": false, 257 | "asc": true, 258 | "nulls": "last" 259 | } 260 | ], 261 | "isUnique": false, 262 | "concurrently": false, 263 | "method": "btree", 264 | "with": {} 265 | } 266 | }, 267 | "foreignKeys": {}, 268 | "compositePrimaryKeys": {}, 269 | "uniqueConstraints": { 270 | "station_uid_unique": { 271 | "name": "station_uid_unique", 272 | "nullsNotDistinct": false, 273 | "columns": [ 274 | "uid" 275 | ] 276 | }, 277 | "station_id_unique": { 278 | "name": "station_id_unique", 279 | "nullsNotDistinct": false, 280 | "columns": [ 281 | "id" 282 | ] 283 | } 284 | } 285 | } 286 | }, 287 | "enums": { 288 | "public.station_type": { 289 | "name": "station_type", 290 | "schema": "public", 291 | "values": [ 292 | "KRL", 293 | "MRT", 294 | "LRT", 295 | "LOCAL" 296 | ] 297 | } 298 | }, 299 | "schemas": {}, 300 | "sequences": {}, 301 | "_meta": { 302 | "columns": {}, 303 | "schemas": {}, 304 | "tables": {} 305 | } 306 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0003_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "d2d9cd87-d69d-46ff-9890-21be36c0cb3b", 3 | "prevId": "5db983ea-4af8-40b5-9bca-1f274a3c4e3a", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.station": { 8 | "name": "station", 9 | "schema": "", 10 | "columns": { 11 | "uid": { 12 | "name": "uid", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "id": { 18 | "name": "id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "name": { 24 | "name": "name", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "type": { 30 | "name": "type", 31 | "type": "station_type", 32 | "typeSchema": "public", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "metadata": { 37 | "name": "metadata", 38 | "type": "jsonb", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp with time zone", 45 | "primaryKey": false, 46 | "notNull": false, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp with time zone", 52 | "primaryKey": false, 53 | "notNull": false, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": { 58 | "station_uidx": { 59 | "name": "station_uidx", 60 | "columns": [ 61 | { 62 | "expression": "uid", 63 | "isExpression": false, 64 | "asc": true, 65 | "nulls": "last" 66 | } 67 | ], 68 | "isUnique": true, 69 | "concurrently": false, 70 | "method": "btree", 71 | "with": {} 72 | }, 73 | "station_idx": { 74 | "name": "station_idx", 75 | "columns": [ 76 | { 77 | "expression": "id", 78 | "isExpression": false, 79 | "asc": true, 80 | "nulls": "last" 81 | } 82 | ], 83 | "isUnique": false, 84 | "concurrently": false, 85 | "method": "btree", 86 | "with": {} 87 | }, 88 | "station_type_idx": { 89 | "name": "station_type_idx", 90 | "columns": [ 91 | { 92 | "expression": "type", 93 | "isExpression": false, 94 | "asc": true, 95 | "nulls": "last" 96 | } 97 | ], 98 | "isUnique": false, 99 | "concurrently": false, 100 | "method": "btree", 101 | "with": {} 102 | } 103 | }, 104 | "foreignKeys": {}, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": { 107 | "station_uid_unique": { 108 | "name": "station_uid_unique", 109 | "nullsNotDistinct": false, 110 | "columns": [ 111 | "uid" 112 | ] 113 | }, 114 | "station_id_unique": { 115 | "name": "station_id_unique", 116 | "nullsNotDistinct": false, 117 | "columns": [ 118 | "id" 119 | ] 120 | } 121 | } 122 | }, 123 | "public.schedule": { 124 | "name": "schedule", 125 | "schema": "", 126 | "columns": { 127 | "id": { 128 | "name": "id", 129 | "type": "text", 130 | "primaryKey": true, 131 | "notNull": true 132 | }, 133 | "station_id": { 134 | "name": "station_id", 135 | "type": "text", 136 | "primaryKey": false, 137 | "notNull": true 138 | }, 139 | "station_origin_id": { 140 | "name": "station_origin_id", 141 | "type": "text", 142 | "primaryKey": false, 143 | "notNull": false 144 | }, 145 | "station_destination_id": { 146 | "name": "station_destination_id", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": false 150 | }, 151 | "train_id": { 152 | "name": "train_id", 153 | "type": "text", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "line": { 158 | "name": "line", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "route": { 164 | "name": "route", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "time_departure": { 170 | "name": "time_departure", 171 | "type": "time", 172 | "primaryKey": false, 173 | "notNull": true 174 | }, 175 | "time_at_destination": { 176 | "name": "time_at_destination", 177 | "type": "time", 178 | "primaryKey": false, 179 | "notNull": true 180 | }, 181 | "metadata": { 182 | "name": "metadata", 183 | "type": "jsonb", 184 | "primaryKey": false, 185 | "notNull": false 186 | }, 187 | "created_at": { 188 | "name": "created_at", 189 | "type": "timestamp with time zone", 190 | "primaryKey": false, 191 | "notNull": false, 192 | "default": "now()" 193 | }, 194 | "updated_at": { 195 | "name": "updated_at", 196 | "type": "timestamp with time zone", 197 | "primaryKey": false, 198 | "notNull": false, 199 | "default": "now()" 200 | } 201 | }, 202 | "indexes": { 203 | "schedule_idx": { 204 | "name": "schedule_idx", 205 | "columns": [ 206 | { 207 | "expression": "id", 208 | "isExpression": false, 209 | "asc": true, 210 | "nulls": "last" 211 | } 212 | ], 213 | "isUnique": true, 214 | "concurrently": false, 215 | "method": "btree", 216 | "with": {} 217 | }, 218 | "schedule_station_idx": { 219 | "name": "schedule_station_idx", 220 | "columns": [ 221 | { 222 | "expression": "station_id", 223 | "isExpression": false, 224 | "asc": true, 225 | "nulls": "last" 226 | } 227 | ], 228 | "isUnique": false, 229 | "concurrently": false, 230 | "method": "btree", 231 | "with": {} 232 | }, 233 | "schedule_train_idx": { 234 | "name": "schedule_train_idx", 235 | "columns": [ 236 | { 237 | "expression": "train_id", 238 | "isExpression": false, 239 | "asc": true, 240 | "nulls": "last" 241 | } 242 | ], 243 | "isUnique": false, 244 | "concurrently": false, 245 | "method": "btree", 246 | "with": {} 247 | } 248 | }, 249 | "foreignKeys": { 250 | "schedule_station_id_station_id_fk": { 251 | "name": "schedule_station_id_station_id_fk", 252 | "tableFrom": "schedule", 253 | "tableTo": "station", 254 | "columnsFrom": [ 255 | "station_id" 256 | ], 257 | "columnsTo": [ 258 | "id" 259 | ], 260 | "onDelete": "cascade", 261 | "onUpdate": "no action" 262 | }, 263 | "schedule_station_origin_id_station_id_fk": { 264 | "name": "schedule_station_origin_id_station_id_fk", 265 | "tableFrom": "schedule", 266 | "tableTo": "station", 267 | "columnsFrom": [ 268 | "station_origin_id" 269 | ], 270 | "columnsTo": [ 271 | "id" 272 | ], 273 | "onDelete": "set null", 274 | "onUpdate": "no action" 275 | }, 276 | "schedule_station_destination_id_station_id_fk": { 277 | "name": "schedule_station_destination_id_station_id_fk", 278 | "tableFrom": "schedule", 279 | "tableTo": "station", 280 | "columnsFrom": [ 281 | "station_destination_id" 282 | ], 283 | "columnsTo": [ 284 | "id" 285 | ], 286 | "onDelete": "set null", 287 | "onUpdate": "no action" 288 | } 289 | }, 290 | "compositePrimaryKeys": {}, 291 | "uniqueConstraints": { 292 | "schedule_id_unique": { 293 | "name": "schedule_id_unique", 294 | "nullsNotDistinct": false, 295 | "columns": [ 296 | "id" 297 | ] 298 | } 299 | } 300 | } 301 | }, 302 | "enums": { 303 | "public.station_type": { 304 | "name": "station_type", 305 | "schema": "public", 306 | "values": [ 307 | "KRL", 308 | "MRT", 309 | "LRT", 310 | "LOCAL" 311 | ] 312 | } 313 | }, 314 | "schemas": {}, 315 | "sequences": {}, 316 | "_meta": { 317 | "columns": {}, 318 | "schemas": {}, 319 | "tables": {} 320 | } 321 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0004_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f1783790-c69c-479f-a903-cc6365ccfed6", 3 | "prevId": "d2d9cd87-d69d-46ff-9890-21be36c0cb3b", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.station": { 8 | "name": "station", 9 | "schema": "", 10 | "columns": { 11 | "uid": { 12 | "name": "uid", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "id": { 18 | "name": "id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "name": { 24 | "name": "name", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "type": { 30 | "name": "type", 31 | "type": "station_type", 32 | "typeSchema": "public", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "metadata": { 37 | "name": "metadata", 38 | "type": "jsonb", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp with time zone", 45 | "primaryKey": false, 46 | "notNull": false, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp with time zone", 52 | "primaryKey": false, 53 | "notNull": false, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": { 58 | "station_uidx": { 59 | "name": "station_uidx", 60 | "columns": [ 61 | { 62 | "expression": "uid", 63 | "isExpression": false, 64 | "asc": true, 65 | "nulls": "last" 66 | } 67 | ], 68 | "isUnique": true, 69 | "concurrently": false, 70 | "method": "btree", 71 | "with": {} 72 | }, 73 | "station_idx": { 74 | "name": "station_idx", 75 | "columns": [ 76 | { 77 | "expression": "id", 78 | "isExpression": false, 79 | "asc": true, 80 | "nulls": "last" 81 | } 82 | ], 83 | "isUnique": false, 84 | "concurrently": false, 85 | "method": "btree", 86 | "with": {} 87 | }, 88 | "station_type_idx": { 89 | "name": "station_type_idx", 90 | "columns": [ 91 | { 92 | "expression": "type", 93 | "isExpression": false, 94 | "asc": true, 95 | "nulls": "last" 96 | } 97 | ], 98 | "isUnique": false, 99 | "concurrently": false, 100 | "method": "btree", 101 | "with": {} 102 | } 103 | }, 104 | "foreignKeys": {}, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": { 107 | "station_uid_unique": { 108 | "name": "station_uid_unique", 109 | "nullsNotDistinct": false, 110 | "columns": [ 111 | "uid" 112 | ] 113 | }, 114 | "station_id_unique": { 115 | "name": "station_id_unique", 116 | "nullsNotDistinct": false, 117 | "columns": [ 118 | "id" 119 | ] 120 | } 121 | } 122 | }, 123 | "public.schedule": { 124 | "name": "schedule", 125 | "schema": "", 126 | "columns": { 127 | "id": { 128 | "name": "id", 129 | "type": "text", 130 | "primaryKey": true, 131 | "notNull": true 132 | }, 133 | "station_id": { 134 | "name": "station_id", 135 | "type": "text", 136 | "primaryKey": false, 137 | "notNull": true 138 | }, 139 | "station_origin_id": { 140 | "name": "station_origin_id", 141 | "type": "text", 142 | "primaryKey": false, 143 | "notNull": false 144 | }, 145 | "station_destination_id": { 146 | "name": "station_destination_id", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": false 150 | }, 151 | "train_id": { 152 | "name": "train_id", 153 | "type": "text", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "line": { 158 | "name": "line", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "route": { 164 | "name": "route", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "departs_at": { 170 | "name": "departs_at", 171 | "type": "timestamp with time zone", 172 | "primaryKey": false, 173 | "notNull": false, 174 | "default": "now()" 175 | }, 176 | "arrives_at": { 177 | "name": "arrives_at", 178 | "type": "timestamp with time zone", 179 | "primaryKey": false, 180 | "notNull": false, 181 | "default": "now()" 182 | }, 183 | "metadata": { 184 | "name": "metadata", 185 | "type": "jsonb", 186 | "primaryKey": false, 187 | "notNull": false 188 | }, 189 | "created_at": { 190 | "name": "created_at", 191 | "type": "timestamp with time zone", 192 | "primaryKey": false, 193 | "notNull": false, 194 | "default": "now()" 195 | }, 196 | "updated_at": { 197 | "name": "updated_at", 198 | "type": "timestamp with time zone", 199 | "primaryKey": false, 200 | "notNull": false, 201 | "default": "now()" 202 | } 203 | }, 204 | "indexes": { 205 | "schedule_idx": { 206 | "name": "schedule_idx", 207 | "columns": [ 208 | { 209 | "expression": "id", 210 | "isExpression": false, 211 | "asc": true, 212 | "nulls": "last" 213 | } 214 | ], 215 | "isUnique": true, 216 | "concurrently": false, 217 | "method": "btree", 218 | "with": {} 219 | }, 220 | "schedule_station_idx": { 221 | "name": "schedule_station_idx", 222 | "columns": [ 223 | { 224 | "expression": "station_id", 225 | "isExpression": false, 226 | "asc": true, 227 | "nulls": "last" 228 | } 229 | ], 230 | "isUnique": false, 231 | "concurrently": false, 232 | "method": "btree", 233 | "with": {} 234 | }, 235 | "schedule_train_idx": { 236 | "name": "schedule_train_idx", 237 | "columns": [ 238 | { 239 | "expression": "train_id", 240 | "isExpression": false, 241 | "asc": true, 242 | "nulls": "last" 243 | } 244 | ], 245 | "isUnique": false, 246 | "concurrently": false, 247 | "method": "btree", 248 | "with": {} 249 | } 250 | }, 251 | "foreignKeys": { 252 | "schedule_station_id_station_id_fk": { 253 | "name": "schedule_station_id_station_id_fk", 254 | "tableFrom": "schedule", 255 | "tableTo": "station", 256 | "columnsFrom": [ 257 | "station_id" 258 | ], 259 | "columnsTo": [ 260 | "id" 261 | ], 262 | "onDelete": "cascade", 263 | "onUpdate": "no action" 264 | }, 265 | "schedule_station_origin_id_station_id_fk": { 266 | "name": "schedule_station_origin_id_station_id_fk", 267 | "tableFrom": "schedule", 268 | "tableTo": "station", 269 | "columnsFrom": [ 270 | "station_origin_id" 271 | ], 272 | "columnsTo": [ 273 | "id" 274 | ], 275 | "onDelete": "set null", 276 | "onUpdate": "no action" 277 | }, 278 | "schedule_station_destination_id_station_id_fk": { 279 | "name": "schedule_station_destination_id_station_id_fk", 280 | "tableFrom": "schedule", 281 | "tableTo": "station", 282 | "columnsFrom": [ 283 | "station_destination_id" 284 | ], 285 | "columnsTo": [ 286 | "id" 287 | ], 288 | "onDelete": "set null", 289 | "onUpdate": "no action" 290 | } 291 | }, 292 | "compositePrimaryKeys": {}, 293 | "uniqueConstraints": { 294 | "schedule_id_unique": { 295 | "name": "schedule_id_unique", 296 | "nullsNotDistinct": false, 297 | "columns": [ 298 | "id" 299 | ] 300 | } 301 | } 302 | } 303 | }, 304 | "enums": { 305 | "public.station_type": { 306 | "name": "station_type", 307 | "schema": "public", 308 | "values": [ 309 | "KRL", 310 | "MRT", 311 | "LRT", 312 | "LOCAL" 313 | ] 314 | } 315 | }, 316 | "schemas": {}, 317 | "sequences": {}, 318 | "_meta": { 319 | "columns": {}, 320 | "schemas": {}, 321 | "tables": {} 322 | } 323 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0005_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "65f42182-0d09-4f8a-b7f5-f593648f5c31", 3 | "prevId": "f1783790-c69c-479f-a903-cc6365ccfed6", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.station": { 8 | "name": "station", 9 | "schema": "", 10 | "columns": { 11 | "uid": { 12 | "name": "uid", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "id": { 18 | "name": "id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "name": { 24 | "name": "name", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "type": { 30 | "name": "type", 31 | "type": "station_type", 32 | "typeSchema": "public", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "metadata": { 37 | "name": "metadata", 38 | "type": "jsonb", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp with time zone", 45 | "primaryKey": false, 46 | "notNull": false, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp with time zone", 52 | "primaryKey": false, 53 | "notNull": false, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": { 58 | "station_uidx": { 59 | "name": "station_uidx", 60 | "columns": [ 61 | { 62 | "expression": "uid", 63 | "isExpression": false, 64 | "asc": true, 65 | "nulls": "last" 66 | } 67 | ], 68 | "isUnique": true, 69 | "concurrently": false, 70 | "method": "btree", 71 | "with": {} 72 | }, 73 | "station_idx": { 74 | "name": "station_idx", 75 | "columns": [ 76 | { 77 | "expression": "id", 78 | "isExpression": false, 79 | "asc": true, 80 | "nulls": "last" 81 | } 82 | ], 83 | "isUnique": false, 84 | "concurrently": false, 85 | "method": "btree", 86 | "with": {} 87 | }, 88 | "station_type_idx": { 89 | "name": "station_type_idx", 90 | "columns": [ 91 | { 92 | "expression": "type", 93 | "isExpression": false, 94 | "asc": true, 95 | "nulls": "last" 96 | } 97 | ], 98 | "isUnique": false, 99 | "concurrently": false, 100 | "method": "btree", 101 | "with": {} 102 | } 103 | }, 104 | "foreignKeys": {}, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": { 107 | "station_uid_unique": { 108 | "name": "station_uid_unique", 109 | "nullsNotDistinct": false, 110 | "columns": [ 111 | "uid" 112 | ] 113 | }, 114 | "station_id_unique": { 115 | "name": "station_id_unique", 116 | "nullsNotDistinct": false, 117 | "columns": [ 118 | "id" 119 | ] 120 | } 121 | } 122 | }, 123 | "public.schedule": { 124 | "name": "schedule", 125 | "schema": "", 126 | "columns": { 127 | "id": { 128 | "name": "id", 129 | "type": "text", 130 | "primaryKey": true, 131 | "notNull": true 132 | }, 133 | "station_id": { 134 | "name": "station_id", 135 | "type": "text", 136 | "primaryKey": false, 137 | "notNull": true 138 | }, 139 | "station_origin_id": { 140 | "name": "station_origin_id", 141 | "type": "text", 142 | "primaryKey": false, 143 | "notNull": false 144 | }, 145 | "station_destination_id": { 146 | "name": "station_destination_id", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": false 150 | }, 151 | "train_id": { 152 | "name": "train_id", 153 | "type": "text", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "line": { 158 | "name": "line", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "route": { 164 | "name": "route", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "departs_at": { 170 | "name": "departs_at", 171 | "type": "timestamp with time zone", 172 | "primaryKey": false, 173 | "notNull": true, 174 | "default": "now()" 175 | }, 176 | "arrives_at": { 177 | "name": "arrives_at", 178 | "type": "timestamp with time zone", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "default": "now()" 182 | }, 183 | "metadata": { 184 | "name": "metadata", 185 | "type": "jsonb", 186 | "primaryKey": false, 187 | "notNull": false 188 | }, 189 | "created_at": { 190 | "name": "created_at", 191 | "type": "timestamp with time zone", 192 | "primaryKey": false, 193 | "notNull": true, 194 | "default": "now()" 195 | }, 196 | "updated_at": { 197 | "name": "updated_at", 198 | "type": "timestamp with time zone", 199 | "primaryKey": false, 200 | "notNull": true, 201 | "default": "now()" 202 | } 203 | }, 204 | "indexes": { 205 | "schedule_idx": { 206 | "name": "schedule_idx", 207 | "columns": [ 208 | { 209 | "expression": "id", 210 | "isExpression": false, 211 | "asc": true, 212 | "nulls": "last" 213 | } 214 | ], 215 | "isUnique": true, 216 | "concurrently": false, 217 | "method": "btree", 218 | "with": {} 219 | }, 220 | "schedule_station_idx": { 221 | "name": "schedule_station_idx", 222 | "columns": [ 223 | { 224 | "expression": "station_id", 225 | "isExpression": false, 226 | "asc": true, 227 | "nulls": "last" 228 | } 229 | ], 230 | "isUnique": false, 231 | "concurrently": false, 232 | "method": "btree", 233 | "with": {} 234 | }, 235 | "schedule_train_idx": { 236 | "name": "schedule_train_idx", 237 | "columns": [ 238 | { 239 | "expression": "train_id", 240 | "isExpression": false, 241 | "asc": true, 242 | "nulls": "last" 243 | } 244 | ], 245 | "isUnique": false, 246 | "concurrently": false, 247 | "method": "btree", 248 | "with": {} 249 | } 250 | }, 251 | "foreignKeys": { 252 | "schedule_station_id_station_id_fk": { 253 | "name": "schedule_station_id_station_id_fk", 254 | "tableFrom": "schedule", 255 | "tableTo": "station", 256 | "columnsFrom": [ 257 | "station_id" 258 | ], 259 | "columnsTo": [ 260 | "id" 261 | ], 262 | "onDelete": "cascade", 263 | "onUpdate": "no action" 264 | }, 265 | "schedule_station_origin_id_station_id_fk": { 266 | "name": "schedule_station_origin_id_station_id_fk", 267 | "tableFrom": "schedule", 268 | "tableTo": "station", 269 | "columnsFrom": [ 270 | "station_origin_id" 271 | ], 272 | "columnsTo": [ 273 | "id" 274 | ], 275 | "onDelete": "set null", 276 | "onUpdate": "no action" 277 | }, 278 | "schedule_station_destination_id_station_id_fk": { 279 | "name": "schedule_station_destination_id_station_id_fk", 280 | "tableFrom": "schedule", 281 | "tableTo": "station", 282 | "columnsFrom": [ 283 | "station_destination_id" 284 | ], 285 | "columnsTo": [ 286 | "id" 287 | ], 288 | "onDelete": "set null", 289 | "onUpdate": "no action" 290 | } 291 | }, 292 | "compositePrimaryKeys": {}, 293 | "uniqueConstraints": { 294 | "schedule_id_unique": { 295 | "name": "schedule_id_unique", 296 | "nullsNotDistinct": false, 297 | "columns": [ 298 | "id" 299 | ] 300 | } 301 | } 302 | } 303 | }, 304 | "enums": { 305 | "public.station_type": { 306 | "name": "station_type", 307 | "schema": "public", 308 | "values": [ 309 | "KRL", 310 | "MRT", 311 | "LRT", 312 | "LOCAL" 313 | ] 314 | } 315 | }, 316 | "schemas": {}, 317 | "sequences": {}, 318 | "_meta": { 319 | "columns": {}, 320 | "schemas": {}, 321 | "tables": {} 322 | } 323 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0006_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bab82009-af8d-4269-8815-e14cb1f6302d", 3 | "prevId": "65f42182-0d09-4f8a-b7f5-f593648f5c31", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.station": { 8 | "name": "station", 9 | "schema": "", 10 | "columns": { 11 | "uid": { 12 | "name": "uid", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "id": { 18 | "name": "id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "name": { 24 | "name": "name", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "type": { 30 | "name": "type", 31 | "type": "station_type", 32 | "typeSchema": "public", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "metadata": { 37 | "name": "metadata", 38 | "type": "jsonb", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp with time zone", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp with time zone", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": { 58 | "station_uidx": { 59 | "name": "station_uidx", 60 | "columns": [ 61 | { 62 | "expression": "uid", 63 | "isExpression": false, 64 | "asc": true, 65 | "nulls": "last" 66 | } 67 | ], 68 | "isUnique": true, 69 | "concurrently": false, 70 | "method": "btree", 71 | "with": {} 72 | }, 73 | "station_idx": { 74 | "name": "station_idx", 75 | "columns": [ 76 | { 77 | "expression": "id", 78 | "isExpression": false, 79 | "asc": true, 80 | "nulls": "last" 81 | } 82 | ], 83 | "isUnique": false, 84 | "concurrently": false, 85 | "method": "btree", 86 | "with": {} 87 | }, 88 | "station_type_idx": { 89 | "name": "station_type_idx", 90 | "columns": [ 91 | { 92 | "expression": "type", 93 | "isExpression": false, 94 | "asc": true, 95 | "nulls": "last" 96 | } 97 | ], 98 | "isUnique": false, 99 | "concurrently": false, 100 | "method": "btree", 101 | "with": {} 102 | } 103 | }, 104 | "foreignKeys": {}, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": { 107 | "station_uid_unique": { 108 | "name": "station_uid_unique", 109 | "nullsNotDistinct": false, 110 | "columns": [ 111 | "uid" 112 | ] 113 | }, 114 | "station_id_unique": { 115 | "name": "station_id_unique", 116 | "nullsNotDistinct": false, 117 | "columns": [ 118 | "id" 119 | ] 120 | } 121 | } 122 | }, 123 | "public.schedule": { 124 | "name": "schedule", 125 | "schema": "", 126 | "columns": { 127 | "id": { 128 | "name": "id", 129 | "type": "text", 130 | "primaryKey": true, 131 | "notNull": true 132 | }, 133 | "station_id": { 134 | "name": "station_id", 135 | "type": "text", 136 | "primaryKey": false, 137 | "notNull": true 138 | }, 139 | "station_origin_id": { 140 | "name": "station_origin_id", 141 | "type": "text", 142 | "primaryKey": false, 143 | "notNull": false 144 | }, 145 | "station_destination_id": { 146 | "name": "station_destination_id", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": false 150 | }, 151 | "train_id": { 152 | "name": "train_id", 153 | "type": "text", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "line": { 158 | "name": "line", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "route": { 164 | "name": "route", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "departs_at": { 170 | "name": "departs_at", 171 | "type": "timestamp with time zone", 172 | "primaryKey": false, 173 | "notNull": true, 174 | "default": "now()" 175 | }, 176 | "arrives_at": { 177 | "name": "arrives_at", 178 | "type": "timestamp with time zone", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "default": "now()" 182 | }, 183 | "metadata": { 184 | "name": "metadata", 185 | "type": "jsonb", 186 | "primaryKey": false, 187 | "notNull": false 188 | }, 189 | "created_at": { 190 | "name": "created_at", 191 | "type": "timestamp with time zone", 192 | "primaryKey": false, 193 | "notNull": true, 194 | "default": "now()" 195 | }, 196 | "updated_at": { 197 | "name": "updated_at", 198 | "type": "timestamp with time zone", 199 | "primaryKey": false, 200 | "notNull": true, 201 | "default": "now()" 202 | } 203 | }, 204 | "indexes": { 205 | "schedule_idx": { 206 | "name": "schedule_idx", 207 | "columns": [ 208 | { 209 | "expression": "id", 210 | "isExpression": false, 211 | "asc": true, 212 | "nulls": "last" 213 | } 214 | ], 215 | "isUnique": true, 216 | "concurrently": false, 217 | "method": "btree", 218 | "with": {} 219 | }, 220 | "schedule_station_idx": { 221 | "name": "schedule_station_idx", 222 | "columns": [ 223 | { 224 | "expression": "station_id", 225 | "isExpression": false, 226 | "asc": true, 227 | "nulls": "last" 228 | } 229 | ], 230 | "isUnique": false, 231 | "concurrently": false, 232 | "method": "btree", 233 | "with": {} 234 | }, 235 | "schedule_train_idx": { 236 | "name": "schedule_train_idx", 237 | "columns": [ 238 | { 239 | "expression": "train_id", 240 | "isExpression": false, 241 | "asc": true, 242 | "nulls": "last" 243 | } 244 | ], 245 | "isUnique": false, 246 | "concurrently": false, 247 | "method": "btree", 248 | "with": {} 249 | } 250 | }, 251 | "foreignKeys": { 252 | "schedule_station_id_station_id_fk": { 253 | "name": "schedule_station_id_station_id_fk", 254 | "tableFrom": "schedule", 255 | "tableTo": "station", 256 | "columnsFrom": [ 257 | "station_id" 258 | ], 259 | "columnsTo": [ 260 | "id" 261 | ], 262 | "onDelete": "cascade", 263 | "onUpdate": "no action" 264 | }, 265 | "schedule_station_origin_id_station_id_fk": { 266 | "name": "schedule_station_origin_id_station_id_fk", 267 | "tableFrom": "schedule", 268 | "tableTo": "station", 269 | "columnsFrom": [ 270 | "station_origin_id" 271 | ], 272 | "columnsTo": [ 273 | "id" 274 | ], 275 | "onDelete": "set null", 276 | "onUpdate": "no action" 277 | }, 278 | "schedule_station_destination_id_station_id_fk": { 279 | "name": "schedule_station_destination_id_station_id_fk", 280 | "tableFrom": "schedule", 281 | "tableTo": "station", 282 | "columnsFrom": [ 283 | "station_destination_id" 284 | ], 285 | "columnsTo": [ 286 | "id" 287 | ], 288 | "onDelete": "set null", 289 | "onUpdate": "no action" 290 | } 291 | }, 292 | "compositePrimaryKeys": {}, 293 | "uniqueConstraints": { 294 | "schedule_id_unique": { 295 | "name": "schedule_id_unique", 296 | "nullsNotDistinct": false, 297 | "columns": [ 298 | "id" 299 | ] 300 | } 301 | } 302 | } 303 | }, 304 | "enums": { 305 | "public.station_type": { 306 | "name": "station_type", 307 | "schema": "public", 308 | "values": [ 309 | "KRL", 310 | "MRT", 311 | "LRT", 312 | "LOCAL" 313 | ] 314 | } 315 | }, 316 | "schemas": {}, 317 | "sequences": {}, 318 | "_meta": { 319 | "columns": {}, 320 | "schemas": {}, 321 | "tables": {} 322 | } 323 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0007_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "511ffc79-573f-46fb-adaf-1de89c7f7d69", 3 | "prevId": "bab82009-af8d-4269-8815-e14cb1f6302d", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.station": { 8 | "name": "station", 9 | "schema": "", 10 | "columns": { 11 | "uid": { 12 | "name": "uid", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "id": { 18 | "name": "id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "name": { 24 | "name": "name", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "type": { 30 | "name": "type", 31 | "type": "station_type", 32 | "typeSchema": "public", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "metadata": { 37 | "name": "metadata", 38 | "type": "jsonb", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp with time zone", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp with time zone", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": { 58 | "station_uidx": { 59 | "name": "station_uidx", 60 | "columns": [ 61 | { 62 | "expression": "uid", 63 | "isExpression": false, 64 | "asc": true, 65 | "nulls": "last" 66 | } 67 | ], 68 | "isUnique": true, 69 | "concurrently": false, 70 | "method": "btree", 71 | "with": {} 72 | }, 73 | "station_idx": { 74 | "name": "station_idx", 75 | "columns": [ 76 | { 77 | "expression": "id", 78 | "isExpression": false, 79 | "asc": true, 80 | "nulls": "last" 81 | } 82 | ], 83 | "isUnique": false, 84 | "concurrently": false, 85 | "method": "btree", 86 | "with": {} 87 | }, 88 | "station_type_idx": { 89 | "name": "station_type_idx", 90 | "columns": [ 91 | { 92 | "expression": "type", 93 | "isExpression": false, 94 | "asc": true, 95 | "nulls": "last" 96 | } 97 | ], 98 | "isUnique": false, 99 | "concurrently": false, 100 | "method": "btree", 101 | "with": {} 102 | } 103 | }, 104 | "foreignKeys": {}, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": { 107 | "station_uid_unique": { 108 | "name": "station_uid_unique", 109 | "nullsNotDistinct": false, 110 | "columns": [ 111 | "uid" 112 | ] 113 | }, 114 | "station_id_unique": { 115 | "name": "station_id_unique", 116 | "nullsNotDistinct": false, 117 | "columns": [ 118 | "id" 119 | ] 120 | } 121 | } 122 | }, 123 | "public.schedule": { 124 | "name": "schedule", 125 | "schema": "", 126 | "columns": { 127 | "id": { 128 | "name": "id", 129 | "type": "text", 130 | "primaryKey": true, 131 | "notNull": true 132 | }, 133 | "station_id": { 134 | "name": "station_id", 135 | "type": "text", 136 | "primaryKey": false, 137 | "notNull": true 138 | }, 139 | "station_origin_id": { 140 | "name": "station_origin_id", 141 | "type": "text", 142 | "primaryKey": false, 143 | "notNull": true 144 | }, 145 | "station_destination_id": { 146 | "name": "station_destination_id", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": true 150 | }, 151 | "train_id": { 152 | "name": "train_id", 153 | "type": "text", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "line": { 158 | "name": "line", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "route": { 164 | "name": "route", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "departs_at": { 170 | "name": "departs_at", 171 | "type": "timestamp with time zone", 172 | "primaryKey": false, 173 | "notNull": true, 174 | "default": "now()" 175 | }, 176 | "arrives_at": { 177 | "name": "arrives_at", 178 | "type": "timestamp with time zone", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "default": "now()" 182 | }, 183 | "metadata": { 184 | "name": "metadata", 185 | "type": "jsonb", 186 | "primaryKey": false, 187 | "notNull": false 188 | }, 189 | "created_at": { 190 | "name": "created_at", 191 | "type": "timestamp with time zone", 192 | "primaryKey": false, 193 | "notNull": true, 194 | "default": "now()" 195 | }, 196 | "updated_at": { 197 | "name": "updated_at", 198 | "type": "timestamp with time zone", 199 | "primaryKey": false, 200 | "notNull": true, 201 | "default": "now()" 202 | } 203 | }, 204 | "indexes": { 205 | "schedule_idx": { 206 | "name": "schedule_idx", 207 | "columns": [ 208 | { 209 | "expression": "id", 210 | "isExpression": false, 211 | "asc": true, 212 | "nulls": "last" 213 | } 214 | ], 215 | "isUnique": true, 216 | "concurrently": false, 217 | "method": "btree", 218 | "with": {} 219 | }, 220 | "schedule_station_idx": { 221 | "name": "schedule_station_idx", 222 | "columns": [ 223 | { 224 | "expression": "station_id", 225 | "isExpression": false, 226 | "asc": true, 227 | "nulls": "last" 228 | } 229 | ], 230 | "isUnique": false, 231 | "concurrently": false, 232 | "method": "btree", 233 | "with": {} 234 | }, 235 | "schedule_train_idx": { 236 | "name": "schedule_train_idx", 237 | "columns": [ 238 | { 239 | "expression": "train_id", 240 | "isExpression": false, 241 | "asc": true, 242 | "nulls": "last" 243 | } 244 | ], 245 | "isUnique": false, 246 | "concurrently": false, 247 | "method": "btree", 248 | "with": {} 249 | } 250 | }, 251 | "foreignKeys": { 252 | "schedule_station_id_station_id_fk": { 253 | "name": "schedule_station_id_station_id_fk", 254 | "tableFrom": "schedule", 255 | "tableTo": "station", 256 | "columnsFrom": [ 257 | "station_id" 258 | ], 259 | "columnsTo": [ 260 | "id" 261 | ], 262 | "onDelete": "cascade", 263 | "onUpdate": "no action" 264 | }, 265 | "schedule_station_origin_id_station_id_fk": { 266 | "name": "schedule_station_origin_id_station_id_fk", 267 | "tableFrom": "schedule", 268 | "tableTo": "station", 269 | "columnsFrom": [ 270 | "station_origin_id" 271 | ], 272 | "columnsTo": [ 273 | "id" 274 | ], 275 | "onDelete": "set null", 276 | "onUpdate": "no action" 277 | }, 278 | "schedule_station_destination_id_station_id_fk": { 279 | "name": "schedule_station_destination_id_station_id_fk", 280 | "tableFrom": "schedule", 281 | "tableTo": "station", 282 | "columnsFrom": [ 283 | "station_destination_id" 284 | ], 285 | "columnsTo": [ 286 | "id" 287 | ], 288 | "onDelete": "set null", 289 | "onUpdate": "no action" 290 | } 291 | }, 292 | "compositePrimaryKeys": {}, 293 | "uniqueConstraints": { 294 | "schedule_id_unique": { 295 | "name": "schedule_id_unique", 296 | "nullsNotDistinct": false, 297 | "columns": [ 298 | "id" 299 | ] 300 | } 301 | } 302 | } 303 | }, 304 | "enums": { 305 | "public.station_type": { 306 | "name": "station_type", 307 | "schema": "public", 308 | "values": [ 309 | "KRL", 310 | "MRT", 311 | "LRT", 312 | "LOCAL" 313 | ] 314 | } 315 | }, 316 | "schemas": {}, 317 | "sequences": {}, 318 | "_meta": { 319 | "columns": {}, 320 | "schemas": {}, 321 | "tables": {} 322 | } 323 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/0008_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ae343a69-44a3-4c74-9916-0e7ac4f8f116", 3 | "prevId": "511ffc79-573f-46fb-adaf-1de89c7f7d69", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.station": { 8 | "name": "station", 9 | "schema": "", 10 | "columns": { 11 | "uid": { 12 | "name": "uid", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "id": { 18 | "name": "id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "name": { 24 | "name": "name", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "type": { 30 | "name": "type", 31 | "type": "station_type", 32 | "typeSchema": "public", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "metadata": { 37 | "name": "metadata", 38 | "type": "jsonb", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp with time zone", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp with time zone", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": { 58 | "station_uidx": { 59 | "name": "station_uidx", 60 | "columns": [ 61 | { 62 | "expression": "uid", 63 | "isExpression": false, 64 | "asc": true, 65 | "nulls": "last" 66 | } 67 | ], 68 | "isUnique": true, 69 | "concurrently": false, 70 | "method": "btree", 71 | "with": {} 72 | }, 73 | "station_idx": { 74 | "name": "station_idx", 75 | "columns": [ 76 | { 77 | "expression": "id", 78 | "isExpression": false, 79 | "asc": true, 80 | "nulls": "last" 81 | } 82 | ], 83 | "isUnique": false, 84 | "concurrently": false, 85 | "method": "btree", 86 | "with": {} 87 | }, 88 | "station_type_idx": { 89 | "name": "station_type_idx", 90 | "columns": [ 91 | { 92 | "expression": "type", 93 | "isExpression": false, 94 | "asc": true, 95 | "nulls": "last" 96 | } 97 | ], 98 | "isUnique": false, 99 | "concurrently": false, 100 | "method": "btree", 101 | "with": {} 102 | } 103 | }, 104 | "foreignKeys": {}, 105 | "compositePrimaryKeys": {}, 106 | "uniqueConstraints": { 107 | "station_uid_unique": { 108 | "name": "station_uid_unique", 109 | "nullsNotDistinct": false, 110 | "columns": [ 111 | "uid" 112 | ] 113 | }, 114 | "station_id_unique": { 115 | "name": "station_id_unique", 116 | "nullsNotDistinct": false, 117 | "columns": [ 118 | "id" 119 | ] 120 | } 121 | } 122 | }, 123 | "public.schedule": { 124 | "name": "schedule", 125 | "schema": "", 126 | "columns": { 127 | "id": { 128 | "name": "id", 129 | "type": "text", 130 | "primaryKey": true, 131 | "notNull": true 132 | }, 133 | "station_id": { 134 | "name": "station_id", 135 | "type": "text", 136 | "primaryKey": false, 137 | "notNull": true 138 | }, 139 | "station_origin_id": { 140 | "name": "station_origin_id", 141 | "type": "text", 142 | "primaryKey": false, 143 | "notNull": true 144 | }, 145 | "station_destination_id": { 146 | "name": "station_destination_id", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": true 150 | }, 151 | "train_id": { 152 | "name": "train_id", 153 | "type": "text", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "line": { 158 | "name": "line", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "route": { 164 | "name": "route", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "departs_at": { 170 | "name": "departs_at", 171 | "type": "timestamp with time zone", 172 | "primaryKey": false, 173 | "notNull": true, 174 | "default": "now()" 175 | }, 176 | "arrives_at": { 177 | "name": "arrives_at", 178 | "type": "timestamp with time zone", 179 | "primaryKey": false, 180 | "notNull": true, 181 | "default": "now()" 182 | }, 183 | "metadata": { 184 | "name": "metadata", 185 | "type": "jsonb", 186 | "primaryKey": false, 187 | "notNull": false 188 | }, 189 | "created_at": { 190 | "name": "created_at", 191 | "type": "timestamp with time zone", 192 | "primaryKey": false, 193 | "notNull": true, 194 | "default": "now()" 195 | }, 196 | "updated_at": { 197 | "name": "updated_at", 198 | "type": "timestamp with time zone", 199 | "primaryKey": false, 200 | "notNull": true, 201 | "default": "now()" 202 | } 203 | }, 204 | "indexes": { 205 | "schedule_idx": { 206 | "name": "schedule_idx", 207 | "columns": [ 208 | { 209 | "expression": "id", 210 | "isExpression": false, 211 | "asc": true, 212 | "nulls": "last" 213 | } 214 | ], 215 | "isUnique": true, 216 | "concurrently": false, 217 | "method": "btree", 218 | "with": {} 219 | }, 220 | "schedule_station_idx": { 221 | "name": "schedule_station_idx", 222 | "columns": [ 223 | { 224 | "expression": "station_id", 225 | "isExpression": false, 226 | "asc": true, 227 | "nulls": "last" 228 | } 229 | ], 230 | "isUnique": false, 231 | "concurrently": false, 232 | "method": "btree", 233 | "with": {} 234 | }, 235 | "schedule_train_idx": { 236 | "name": "schedule_train_idx", 237 | "columns": [ 238 | { 239 | "expression": "train_id", 240 | "isExpression": false, 241 | "asc": true, 242 | "nulls": "last" 243 | } 244 | ], 245 | "isUnique": false, 246 | "concurrently": false, 247 | "method": "btree", 248 | "with": {} 249 | } 250 | }, 251 | "foreignKeys": { 252 | "schedule_station_id_station_id_fk": { 253 | "name": "schedule_station_id_station_id_fk", 254 | "tableFrom": "schedule", 255 | "tableTo": "station", 256 | "columnsFrom": [ 257 | "station_id" 258 | ], 259 | "columnsTo": [ 260 | "id" 261 | ], 262 | "onDelete": "cascade", 263 | "onUpdate": "no action" 264 | }, 265 | "schedule_station_origin_id_station_id_fk": { 266 | "name": "schedule_station_origin_id_station_id_fk", 267 | "tableFrom": "schedule", 268 | "tableTo": "station", 269 | "columnsFrom": [ 270 | "station_origin_id" 271 | ], 272 | "columnsTo": [ 273 | "id" 274 | ], 275 | "onDelete": "cascade", 276 | "onUpdate": "no action" 277 | }, 278 | "schedule_station_destination_id_station_id_fk": { 279 | "name": "schedule_station_destination_id_station_id_fk", 280 | "tableFrom": "schedule", 281 | "tableTo": "station", 282 | "columnsFrom": [ 283 | "station_destination_id" 284 | ], 285 | "columnsTo": [ 286 | "id" 287 | ], 288 | "onDelete": "set null", 289 | "onUpdate": "no action" 290 | } 291 | }, 292 | "compositePrimaryKeys": {}, 293 | "uniqueConstraints": { 294 | "schedule_id_unique": { 295 | "name": "schedule_id_unique", 296 | "nullsNotDistinct": false, 297 | "columns": [ 298 | "id" 299 | ] 300 | } 301 | } 302 | } 303 | }, 304 | "enums": { 305 | "public.station_type": { 306 | "name": "station_type", 307 | "schema": "public", 308 | "values": [ 309 | "KRL", 310 | "MRT", 311 | "LRT", 312 | "LOCAL" 313 | ] 314 | } 315 | }, 316 | "schemas": {}, 317 | "sequences": {}, 318 | "_meta": { 319 | "columns": {}, 320 | "schemas": {}, 321 | "tables": {} 322 | } 323 | } -------------------------------------------------------------------------------- /drizzle/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1731395911889, 9 | "tag": "0000_talented_daimon_hellstrom", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1731396377710, 16 | "tag": "0001_tiny_shadowcat", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1731399355344, 23 | "tag": "0002_serious_the_hand", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "7", 29 | "when": 1731404897712, 30 | "tag": "0003_first_dorian_gray", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "7", 36 | "when": 1731486602497, 37 | "tag": "0004_great_vance_astro", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "7", 43 | "when": 1731487109892, 44 | "tag": "0005_rare_senator_kelly", 45 | "breakpoints": true 46 | }, 47 | { 48 | "idx": 6, 49 | "version": "7", 50 | "when": 1731489275577, 51 | "tag": "0006_hesitant_hedge_knight", 52 | "breakpoints": true 53 | }, 54 | { 55 | "idx": 7, 56 | "version": "7", 57 | "when": 1732445107060, 58 | "tag": "0007_naive_pepper_potts", 59 | "breakpoints": true 60 | }, 61 | { 62 | "idx": 8, 63 | "version": "7", 64 | "when": 1743344871226, 65 | "tag": "0008_wooden_purple_man", 66 | "breakpoints": true 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@comuline/api", 3 | "author": { 4 | "name": "Comuline", 5 | "url": "https://github.com/comuline", 6 | "email": "support@comuline.com" 7 | }, 8 | "version": "2.0", 9 | "scripts": { 10 | "dev": "wrangler dev", 11 | "build": "tsup --clean", 12 | "docker:up": "docker-compose -p comuline-api up -d", 13 | "docker:down": "docker-compose down", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "deploy": "wrangler deploy --minify src/index.ts --name comuline-api", 16 | "migrate:drop": "drizzle-kit drop", 17 | "migrate:generate": "drizzle-kit generate", 18 | "migrate:apply": "bun run src/db/migrate.ts", 19 | "format": "prettier -w .", 20 | "format:check": "prettier -c .", 21 | "prepare": "husky", 22 | "sync:schedule": "bun run --env-file .dev.vars src/sync/schedule.ts", 23 | "sync:station": "bun run --env-file .dev.vars src/sync/station.ts" 24 | }, 25 | "dependencies": { 26 | "@hono/zod-openapi": "^0.16.0", 27 | "@neondatabase/serverless": "^0.9.5", 28 | "@scalar/hono-api-reference": "^0.5.145", 29 | "@upstash/redis": "^1.34.3", 30 | "dotenv": "^16.4.5", 31 | "drizzle-orm": "^0.33.0", 32 | "drizzle-zod": "^0.5.1", 33 | "hono": "^4.5.11", 34 | "postgres": "^3.4.4", 35 | "zod": "^3.23.8" 36 | }, 37 | "devDependencies": { 38 | "@cloudflare/workers-types": "^4.20240903.0", 39 | "bun-types": "latest", 40 | "drizzle-kit": "^0.24.2", 41 | "husky": "^9.0.11", 42 | "lint-staged": "^15.2.2", 43 | "prettier": "^3.2.5", 44 | "tsup": "^8.3.5", 45 | "typescript": "^5.6.3", 46 | "wrangler": "^4.6.0" 47 | }, 48 | "module": "src/index.js", 49 | "lint-staged": { 50 | "**/*.{js,jsx,ts,tsx}": [ 51 | "prettier -w" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv" 2 | import { drizzle } from "drizzle-orm/postgres-js" 3 | import { migrate } from "drizzle-orm/postgres-js/migrator" 4 | import postgres from "postgres" 5 | 6 | config({ path: ".dev.vars" }) 7 | 8 | const url = `${process.env.DATABASE_URL}` 9 | const db = drizzle(postgres(url)) 10 | 11 | const main = async () => { 12 | console.info("Migrating database") 13 | await migrate(db, { migrationsFolder: "drizzle/migrations" }) 14 | console.log("Migration complete") 15 | process.exit(0) 16 | } 17 | 18 | main() 19 | -------------------------------------------------------------------------------- /src/db/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./station.table" 2 | export * from "./schedule.table" 3 | -------------------------------------------------------------------------------- /src/db/schema/schedule.table.ts: -------------------------------------------------------------------------------- 1 | import { 2 | index, 3 | jsonb, 4 | pgTable, 5 | text, 6 | time, 7 | timestamp, 8 | uniqueIndex, 9 | } from "drizzle-orm/pg-core" 10 | import { createSelectSchema } from "drizzle-zod" 11 | import { z } from "zod" 12 | import { stationTable } from "./station.table" 13 | import { relations } from "drizzle-orm" 14 | 15 | export const stationScheduleMetadata = z.object({ 16 | /** Origin metadata */ 17 | origin: z.object({ 18 | color: z.string().nullable(), 19 | }), 20 | }) 21 | 22 | export type StationScheduleMetadata = z.infer 23 | 24 | export const scheduleTable = pgTable( 25 | "schedule", 26 | { 27 | id: text("id").primaryKey().unique().notNull(), 28 | station_id: text("station_id") 29 | .notNull() 30 | .references(() => stationTable.id, { 31 | onDelete: "cascade", 32 | }), 33 | station_origin_id: text("station_origin_id") 34 | .notNull() 35 | .references(() => stationTable.id, { 36 | onDelete: "cascade", 37 | }), 38 | station_destination_id: text("station_destination_id") 39 | .references(() => stationTable.id, { 40 | onDelete: "set null", 41 | }) 42 | .notNull(), 43 | train_id: text("train_id").notNull(), 44 | line: text("line").notNull(), 45 | route: text("route").notNull(), 46 | departs_at: timestamp("departs_at", { 47 | mode: "string", 48 | withTimezone: true, 49 | }) 50 | .notNull() 51 | .defaultNow(), 52 | arrives_at: timestamp("arrives_at", { 53 | mode: "string", 54 | withTimezone: true, 55 | }) 56 | .notNull() 57 | .defaultNow(), 58 | metadata: jsonb("metadata").$type(), 59 | created_at: timestamp("created_at", { 60 | mode: "string", 61 | withTimezone: true, 62 | }) 63 | .notNull() 64 | .defaultNow(), 65 | updated_at: timestamp("updated_at", { 66 | withTimezone: true, 67 | mode: "string", 68 | }) 69 | .notNull() 70 | .defaultNow(), 71 | }, 72 | (table) => { 73 | return { 74 | schedule_idx: uniqueIndex("schedule_idx").on(table.id), 75 | schedule_station_idx: index("schedule_station_idx").on(table.station_id), 76 | schedule_train_idx: index("schedule_train_idx").on(table.train_id), 77 | } 78 | }, 79 | ) 80 | 81 | export const scheduleTableRelations = relations(scheduleTable, ({ one }) => ({ 82 | station: one(stationTable, { 83 | fields: [scheduleTable.station_id], 84 | references: [stationTable.id], 85 | }), 86 | station_origin: one(stationTable, { 87 | fields: [scheduleTable.station_origin_id], 88 | references: [stationTable.id], 89 | }), 90 | station_destination: one(stationTable, { 91 | fields: [scheduleTable.station_destination_id], 92 | references: [stationTable.id], 93 | }), 94 | })) 95 | 96 | export const scheduleSchema = createSelectSchema(scheduleTable, { 97 | metadata: stationScheduleMetadata.nullable(), 98 | }) 99 | 100 | export type Schedule = z.infer 101 | 102 | export type NewSchedule = typeof scheduleTable.$inferInsert 103 | -------------------------------------------------------------------------------- /src/db/schema/station.table.ts: -------------------------------------------------------------------------------- 1 | import { 2 | index, 3 | jsonb, 4 | pgEnum, 5 | pgTable, 6 | text, 7 | timestamp, 8 | uniqueIndex, 9 | } from "drizzle-orm/pg-core" 10 | import { createSelectSchema } from "drizzle-zod" 11 | import { z } from "zod" 12 | 13 | /** Station Metadata */ 14 | const stationMetadata = z.object({ 15 | /** Comuline metadata */ 16 | active: z.boolean().optional(), 17 | /** Origin metadata */ 18 | origin: z.object({ 19 | /** KRL */ 20 | daop: z.number().nullable(), 21 | fg_enable: z.number().nullable(), 22 | }), 23 | }) 24 | 25 | export type StationMetadata = z.infer 26 | 27 | export const stationTypeEnum = pgEnum("station_type", [ 28 | "KRL", 29 | "MRT", 30 | "LRT", 31 | "LOCAL", 32 | ]) 33 | 34 | export const stationTable = pgTable( 35 | "station", 36 | { 37 | uid: text("uid").primaryKey().unique().notNull(), 38 | id: text("id").unique().notNull(), 39 | name: text("name").notNull(), 40 | type: stationTypeEnum("type").notNull(), 41 | metadata: jsonb("metadata").$type(), 42 | created_at: timestamp("created_at", { 43 | withTimezone: true, 44 | mode: "string", 45 | }) 46 | .notNull() 47 | .defaultNow(), 48 | updated_at: timestamp("updated_at", { 49 | withTimezone: true, 50 | mode: "string", 51 | }) 52 | .notNull() 53 | .defaultNow(), 54 | }, 55 | (table) => { 56 | return { 57 | station_uidx: uniqueIndex("station_uidx").on(table.uid), 58 | station_idx: index("station_idx").on(table.id), 59 | type_idx: index("station_type_idx").on(table.type), 60 | } 61 | }, 62 | ) 63 | 64 | export const stationSchema = createSelectSchema(stationTable) 65 | 66 | export type NewStation = typeof stationTable.$inferInsert 67 | 68 | export type Station = z.infer 69 | 70 | export type StationType = Station["type"] 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { apiReference } from "@scalar/hono-api-reference" 2 | import { createAPI } from "./modules/api" 3 | import v1 from "./modules/v1" 4 | import { Database } from "./modules/v1/database" 5 | import { HTTPException } from "hono/http-exception" 6 | import { constructResponse } from "./utils/response" 7 | import { trimTrailingSlash } from "hono/trailing-slash" 8 | import { cors } from "hono/cors" 9 | 10 | const api = createAPI() 11 | 12 | const app = api 13 | .doc("/openapi", (c) => ({ 14 | openapi: "3.0.0", 15 | info: { 16 | version: "1.0.0", 17 | title: "Comuline API", 18 | }, 19 | servers: [ 20 | { 21 | url: new URL(c.req.url).origin, 22 | description: c.env.COMULINE_ENV, 23 | }, 24 | ], 25 | })) 26 | .use(trimTrailingSlash()) 27 | .use("*", async (c, next) => 28 | cors({ 29 | origin: (o) => o, 30 | allowMethods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"], 31 | allowHeaders: ["Origin", "Content-Type"], 32 | credentials: true, 33 | })(c, next), 34 | ) 35 | .use(async (c, next) => { 36 | const { db } = new Database({ 37 | COMULINE_ENV: c.env.COMULINE_ENV, 38 | DATABASE_URL: c.env.DATABASE_URL, 39 | }) 40 | c.set("db", db) 41 | c.set("constructResponse", constructResponse) 42 | await next() 43 | }) 44 | .route("/v1", v1) 45 | .use( 46 | "/docs", 47 | apiReference({ 48 | cdn: "https://cdn.jsdelivr.net/npm/@scalar/api-reference", 49 | spec: { 50 | url: "/openapi", 51 | }, 52 | }), 53 | ) 54 | .get("/status", (c) => c.json({ status: "ok" })) 55 | .get("/", (c) => c.redirect("/docs")) 56 | .notFound(() => { 57 | throw new HTTPException(404, { message: "Not found" }) 58 | }) 59 | .onError((err, c) => { 60 | if (err instanceof HTTPException) { 61 | return c.json( 62 | { 63 | metadata: { 64 | success: false, 65 | message: err.message, 66 | cause: err.cause, 67 | }, 68 | }, 69 | err.status, 70 | ) 71 | } 72 | return c.json( 73 | { 74 | metadata: { 75 | success: false, 76 | message: err.message, 77 | cause: err.cause, 78 | }, 79 | }, 80 | 500, 81 | ) 82 | }) 83 | 84 | export default app 85 | -------------------------------------------------------------------------------- /src/modules/api.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIHono } from "@hono/zod-openapi" 2 | import { type Environments } from "@/type" 3 | 4 | export const createAPI = () => 5 | new OpenAPIHono({ strict: true }) 6 | -------------------------------------------------------------------------------- /src/modules/v1/cache.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis/cloudflare" 2 | 3 | export class Cache { 4 | protected kv: Redis 5 | public key: string 6 | 7 | constructor( 8 | protected env: { 9 | UPSTASH_REDIS_REST_TOKEN: string 10 | UPSTASH_REDIS_REST_URL: string 11 | }, 12 | key: string, 13 | ) { 14 | this.key = key 15 | this.kv = Redis.fromEnv(env) 16 | } 17 | 18 | async get(): Promise { 19 | const data = await this.kv.get(this.key) 20 | return data ?? null 21 | } 22 | 23 | async set(value: T, ttl?: number): Promise { 24 | await this.kv.set( 25 | this.key, 26 | JSON.stringify(value), 27 | ttl 28 | ? { 29 | ex: ttl, 30 | } 31 | : undefined, 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/v1/database.ts: -------------------------------------------------------------------------------- 1 | import { neonConfig, Pool } from "@neondatabase/serverless" 2 | import { drizzle, NeonDatabase } from "drizzle-orm/neon-serverless" 3 | import * as schema from "@/db/schema" 4 | 5 | export class Database< 6 | T extends { 7 | DATABASE_URL: string 8 | COMULINE_ENV: string 9 | }, 10 | > { 11 | db: NeonDatabase 12 | 13 | constructor(protected env: T) { 14 | this.db = connectDB(env.DATABASE_URL, env.COMULINE_ENV) 15 | } 16 | } 17 | 18 | export const connectDB = (url: string, env: string) => { 19 | if (env === "development") { 20 | neonConfig.wsProxy = (host) => `${host}:5433/v1` 21 | neonConfig.useSecureWebSocket = false 22 | neonConfig.pipelineTLS = false 23 | neonConfig.pipelineConnect = false 24 | } 25 | 26 | const pool = new Pool({ connectionString: url, ssl: true }) 27 | return drizzle(pool, { schema }) 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { createAPI } from "../api" 2 | import routeController from "./route/route.controller" 3 | import scheduleController from "./schedule/schedule.controller" 4 | import stationController from "./station/station.controller" 5 | 6 | const api = createAPI() 7 | 8 | const v1 = api 9 | .route("/station", stationController) 10 | .route("/schedule", scheduleController) 11 | .route("/route", routeController) 12 | 13 | export default v1 14 | -------------------------------------------------------------------------------- /src/modules/v1/route/route.controller.ts: -------------------------------------------------------------------------------- 1 | import { createRoute, z } from "@hono/zod-openapi" 2 | import { eq, sql } from "drizzle-orm" 3 | import { scheduleTable } from "@/db/schema" 4 | import { buildResponseSchemas } from "@/utils/response" 5 | import { getSecsToMidnight } from "@/utils/time" 6 | import { createAPI } from "@/modules/api" 7 | import { Cache } from "../cache" 8 | import { Route, routeResponseSchema } from "./route.schema" 9 | 10 | const api = createAPI() 11 | 12 | const routeController = api.openapi( 13 | createRoute({ 14 | method: "get", 15 | path: "/{train_id}", 16 | request: { 17 | params: z.object({ 18 | train_id: z 19 | .string() 20 | .min(2) 21 | .openapi({ 22 | param: { 23 | name: "train_id", 24 | in: "path", 25 | }, 26 | default: "2400", 27 | example: "2400", 28 | }), 29 | }), 30 | }, 31 | responses: buildResponseSchemas([ 32 | { 33 | status: 200, 34 | type: "data", 35 | schema: routeResponseSchema, 36 | }, 37 | ]), 38 | tags: ["Route"], 39 | description: "Get sequence of station stop by train ID", 40 | }), 41 | async (c) => { 42 | const param = c.req.valid("param") 43 | const { db } = c.var 44 | 45 | const cache = new Cache(c.env, `route:${param.train_id}`) 46 | 47 | const cached = await cache.get() 48 | 49 | if (cached) 50 | return c.json( 51 | { 52 | metadata: { 53 | success: true, 54 | }, 55 | data: c.var.constructResponse(routeResponseSchema, cached), 56 | }, 57 | 200, 58 | ) 59 | 60 | const query = db.query.scheduleTable 61 | .findMany({ 62 | with: { 63 | station: { 64 | columns: { 65 | name: true, 66 | }, 67 | }, 68 | station_destination: { 69 | columns: { 70 | name: true, 71 | }, 72 | }, 73 | }, 74 | orderBy: (scheduleTable, { asc }) => [asc(scheduleTable.departs_at)], 75 | where: eq(scheduleTable.train_id, sql.placeholder("train_id")), 76 | }) 77 | .prepare("query_route_by_train_id") 78 | 79 | const data = await query.execute({ 80 | train_id: param.train_id, 81 | }) 82 | 83 | if (data.length === 0) 84 | return c.json( 85 | { 86 | metadata: { 87 | success: true, 88 | }, 89 | data: [], 90 | }, 91 | 200, 92 | ) 93 | 94 | const response = { 95 | routes: data.map( 96 | ({ id, station_id, station, departs_at, created_at, updated_at }) => ({ 97 | id, 98 | station_id, 99 | station_name: station.name, 100 | departs_at, 101 | created_at, 102 | updated_at, 103 | }), 104 | ), 105 | details: { 106 | train_id: param.train_id, 107 | line: data[0].line, 108 | route: data[0].route, 109 | station_origin_id: data[0].station_origin_id, 110 | station_origin_name: data[0].station.name, 111 | station_destination_id: data[0].station_destination_id, 112 | station_destination_name: data[0].station_destination?.name ?? "", 113 | arrives_at: data[0].arrives_at, 114 | }, 115 | } satisfies Route 116 | 117 | await cache.set(response, getSecsToMidnight()) 118 | 119 | return c.json( 120 | { 121 | metadata: { 122 | success: true, 123 | }, 124 | data: c.var.constructResponse(routeResponseSchema, response), 125 | }, 126 | 200, 127 | ) 128 | }, 129 | ) 130 | 131 | export default routeController 132 | -------------------------------------------------------------------------------- /src/modules/v1/route/route.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "@hono/zod-openapi" 2 | import { scheduleResponseSchema } from "../schedule/schedule.schema" 3 | import { stationResponseSchema } from "../station/station.schema" 4 | 5 | export const routeResponseSchema = z 6 | .object({ 7 | routes: z.array( 8 | z.object({ 9 | id: scheduleResponseSchema.shape.id, 10 | station_id: scheduleResponseSchema.shape.station_id, 11 | station_name: stationResponseSchema.shape.name.openapi({ 12 | example: "ANCOL", 13 | }), 14 | departs_at: scheduleResponseSchema.shape.departs_at, 15 | created_at: scheduleResponseSchema.shape.created_at, 16 | updated_at: scheduleResponseSchema.shape.updated_at, 17 | }), 18 | ), 19 | details: z.object({ 20 | train_id: scheduleResponseSchema.shape.train_id, 21 | line: scheduleResponseSchema.shape.line, 22 | route: scheduleResponseSchema.shape.route, 23 | station_origin_id: scheduleResponseSchema.shape.station_origin_id, 24 | station_origin_name: stationResponseSchema.shape.name.openapi({ 25 | example: "JAKARTAKOTA", 26 | }), 27 | station_destination_id: 28 | scheduleResponseSchema.shape.station_destination_id, 29 | station_destination_name: z.string().optional().openapi({ 30 | example: "TANJUNGPRIUK", 31 | }), 32 | arrives_at: scheduleResponseSchema.shape.arrives_at, 33 | }), 34 | }) 35 | .openapi("Route") 36 | 37 | export type Route = z.infer 38 | -------------------------------------------------------------------------------- /src/modules/v1/schedule/schedule.controller.ts: -------------------------------------------------------------------------------- 1 | import { createRoute, z } from "@hono/zod-openapi" 2 | import { asc, eq, sql } from "drizzle-orm" 3 | import { scheduleTable, Schedule } from "@/db/schema" 4 | import { createAPI } from "@/modules/api" 5 | import { buildResponseSchemas } from "@/utils/response" 6 | import { scheduleResponseSchema } from "./schedule.schema" 7 | import { Cache } from "../cache" 8 | import { getSecsToMidnight } from "@/utils/time" 9 | 10 | const api = createAPI() 11 | 12 | const scheduleController = api.openapi( 13 | createRoute({ 14 | method: "get", 15 | path: "/{station_id}", 16 | request: { 17 | params: z.object({ 18 | station_id: z 19 | .string() 20 | .min(2) 21 | .openapi({ 22 | param: { 23 | name: "station_id", 24 | in: "path", 25 | }, 26 | default: "AC", 27 | example: "AC", 28 | }), 29 | }), 30 | }, 31 | responses: buildResponseSchemas([ 32 | { 33 | status: 200, 34 | type: "data", 35 | schema: z.array(scheduleResponseSchema), 36 | }, 37 | ]), 38 | tags: ["Schedule"], 39 | description: "Get all schedule by station ID", 40 | }), 41 | async (c) => { 42 | const param = c.req.valid("param") 43 | const { db } = c.var 44 | 45 | const cache = new Cache>( 46 | c.env, 47 | `schedules:${param.station_id}`, 48 | ) 49 | 50 | const cached = await cache.get() 51 | 52 | if (cached) 53 | return c.json( 54 | { 55 | metadata: { 56 | success: true, 57 | }, 58 | data: c.var.constructResponse( 59 | z.array(scheduleResponseSchema), 60 | cached, 61 | ), 62 | }, 63 | 200, 64 | ) 65 | 66 | const query = db 67 | .select() 68 | .from(scheduleTable) 69 | .where(eq(scheduleTable.station_id, sql.placeholder("station_id"))) 70 | .orderBy(asc(scheduleTable.departs_at)) 71 | .prepare("query_schedule_by_station_id") 72 | 73 | const data = await query.execute({ 74 | station_id: param.station_id.toLocaleUpperCase(), 75 | }) 76 | 77 | await cache.set(data, getSecsToMidnight()) 78 | 79 | return c.json( 80 | { 81 | metadata: { 82 | success: true, 83 | }, 84 | data: c.var.constructResponse( 85 | z.array(scheduleResponseSchema), 86 | data.map((x) => { 87 | return x 88 | }), 89 | ), 90 | }, 91 | 200, 92 | ) 93 | }, 94 | ) 95 | 96 | export default scheduleController 97 | -------------------------------------------------------------------------------- /src/modules/v1/schedule/schedule.schema.ts: -------------------------------------------------------------------------------- 1 | import { scheduleSchema, StationScheduleMetadata } from "@/db/schema" 2 | import { z } from "@hono/zod-openapi" 3 | 4 | export const scheduleResponseSchema = z 5 | .object({ 6 | id: scheduleSchema.shape.id.openapi({ 7 | example: "sc_krl_ac_2400", 8 | description: "Schedule unique ID", 9 | }), 10 | station_id: scheduleSchema.shape.station_id.openapi({ 11 | example: "AC", 12 | description: "Station ID where the train stops", 13 | }), 14 | station_origin_id: scheduleSchema.shape.station_origin_id.openapi({ 15 | example: "JAKK", 16 | description: "Station ID where the train originates", 17 | }), 18 | station_destination_id: scheduleSchema.shape.station_destination_id.openapi( 19 | { 20 | example: "TPK", 21 | description: "Station ID where the train terminates", 22 | }, 23 | ), 24 | train_id: scheduleSchema.shape.train_id.openapi({ 25 | example: "2400", 26 | description: "Train ID", 27 | }), 28 | line: scheduleSchema.shape.line.openapi({ 29 | example: "COMMUTER LINE TANJUNGPRIUK", 30 | description: "Train line", 31 | }), 32 | route: scheduleSchema.shape.route.openapi({ 33 | example: "JAKARTAKOTA-TANJUNGPRIUK", 34 | description: "Train route", 35 | }), 36 | departs_at: scheduleSchema.shape.departs_at.openapi({ 37 | format: "date-time", 38 | example: "2024-03-10T09:55:07.213Z", 39 | description: "Train departure time", 40 | }), 41 | arrives_at: scheduleSchema.shape.arrives_at.openapi({ 42 | format: "date-time", 43 | example: "2024-03-10T09:55:09.213Z", 44 | description: "Train arrival time at destination", 45 | }), 46 | metadata: scheduleSchema.shape.metadata.openapi({ 47 | type: "object", 48 | properties: { 49 | origin: { 50 | type: "object", 51 | properties: { 52 | color: { 53 | type: "string", 54 | nullable: true, 55 | }, 56 | }, 57 | }, 58 | }, 59 | example: { 60 | origin: { 61 | color: "#DD0067", 62 | }, 63 | } satisfies StationScheduleMetadata, 64 | }), 65 | created_at: scheduleSchema.shape.created_at.openapi({ 66 | format: "date-time", 67 | example: "2024-03-10T09:55:07.213Z", 68 | }), 69 | updated_at: scheduleSchema.shape.updated_at.openapi({ 70 | format: "date-time", 71 | example: "2024-03-10T09:55:07.213Z", 72 | }), 73 | }) 74 | .openapi("Schedule") satisfies typeof scheduleSchema 75 | -------------------------------------------------------------------------------- /src/modules/v1/station/station.controller.ts: -------------------------------------------------------------------------------- 1 | import { createRoute, z } from "@hono/zod-openapi" 2 | import { eq, sql } from "drizzle-orm" 3 | import { Station, stationTable } from "@/db/schema" 4 | import { buildResponseSchemas } from "@/utils/response" 5 | import { getSecsToMidnight } from "@/utils/time" 6 | import { createAPI } from "@/modules/api" 7 | import { Cache } from "../cache" 8 | import { stationResponseSchema } from "./station.schema" 9 | 10 | const api = createAPI() 11 | 12 | const stationController = api 13 | .openapi( 14 | createRoute({ 15 | method: "get", 16 | path: "/", 17 | responses: buildResponseSchemas([ 18 | { 19 | status: 200, 20 | type: "data", 21 | schema: z.array(stationResponseSchema), 22 | }, 23 | ]), 24 | tags: ["Station"], 25 | description: "Get all station data", 26 | }), 27 | async (c) => { 28 | const { db } = c.var 29 | 30 | const cache = new Cache>(c.env, "stations") 31 | 32 | const cached = await cache.get() 33 | 34 | if (cached) 35 | return c.json( 36 | { 37 | metadata: { 38 | success: true, 39 | }, 40 | data: c.var.constructResponse( 41 | z.array(stationResponseSchema), 42 | cached, 43 | ), 44 | }, 45 | 200, 46 | ) 47 | 48 | const query = db.select().from(stationTable).prepare("query_all_stations") 49 | 50 | const stations = await query.execute() 51 | 52 | await cache.set(stations, getSecsToMidnight()) 53 | 54 | return c.json( 55 | { 56 | metadata: { 57 | success: true, 58 | }, 59 | data: c.var.constructResponse( 60 | z.array(stationResponseSchema), 61 | stations, 62 | ), 63 | }, 64 | 200, 65 | ) 66 | }, 67 | ) 68 | .openapi( 69 | createRoute({ 70 | method: "get", 71 | path: "/{id}", 72 | request: { 73 | params: z.object({ 74 | id: z 75 | .string() 76 | .min(1) 77 | .openapi({ 78 | param: { 79 | name: "id", 80 | in: "path", 81 | }, 82 | default: "MRI", 83 | example: "MRI", 84 | }), 85 | }), 86 | }, 87 | responses: buildResponseSchemas([ 88 | { 89 | status: 200, 90 | type: "data", 91 | schema: stationResponseSchema, 92 | }, 93 | { 94 | status: 404, 95 | type: "metadata", 96 | }, 97 | ]), 98 | tags: ["Station"], 99 | description: "Get station by ID", 100 | }), 101 | async (c) => { 102 | const param = c.req.valid("param") 103 | 104 | const { db } = c.var 105 | 106 | const cache = new Cache(c.env, `station:${param.id}`) 107 | 108 | const cached = await cache.get() 109 | 110 | if (cached) 111 | return c.json( 112 | { 113 | metadata: { 114 | success: true, 115 | }, 116 | data: c.var.constructResponse(stationResponseSchema, cached), 117 | }, 118 | 200, 119 | ) 120 | 121 | const query = db 122 | .select() 123 | .from(stationTable) 124 | .where(eq(stationTable.id, sql.placeholder("id"))) 125 | .prepare("query_station_by_id") 126 | 127 | const data = await query.execute({ id: param.id.toLocaleUpperCase() }) 128 | 129 | if (data.length === 0) 130 | return c.json( 131 | { 132 | metadata: { 133 | success: false, 134 | message: "Station not found", 135 | }, 136 | }, 137 | 404, 138 | ) 139 | 140 | await cache.set(data[0], getSecsToMidnight()) 141 | 142 | return c.json( 143 | { 144 | metadata: { 145 | success: true, 146 | }, 147 | data: c.var.constructResponse(stationResponseSchema, data[0]), 148 | }, 149 | 200, 150 | ) 151 | }, 152 | ) 153 | 154 | export default stationController 155 | -------------------------------------------------------------------------------- /src/modules/v1/station/station.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "@hono/zod-openapi" 2 | import { type StationMetadata, stationSchema } from "@/db/schema" 3 | 4 | export const stationResponseSchema = z 5 | .object({ 6 | uid: stationSchema.shape.uid.openapi({ 7 | example: "st_krl_mri", 8 | }), 9 | id: stationSchema.shape.id.openapi({ 10 | example: "MRI", 11 | }), 12 | name: stationSchema.shape.name.openapi({ 13 | example: "MANGGARAI", 14 | }), 15 | type: stationSchema.shape.type.openapi({ 16 | type: "string", 17 | example: "KRL", 18 | }), 19 | metadata: stationSchema.shape.metadata.openapi({ 20 | type: "object", 21 | properties: { 22 | origin: { 23 | type: "object", 24 | properties: { 25 | daop: { 26 | type: "number", 27 | nullable: true, 28 | }, 29 | fg_enable: { 30 | type: "number", 31 | nullable: true, 32 | }, 33 | }, 34 | }, 35 | }, 36 | example: { 37 | active: true, 38 | origin: { 39 | daop: 1, 40 | fg_enable: 1, 41 | }, 42 | } satisfies StationMetadata, 43 | }), 44 | created_at: stationSchema.shape.created_at.openapi({ 45 | format: "date-time", 46 | example: "2024-03-10T09:55:07.213Z", 47 | }), 48 | updated_at: stationSchema.shape.updated_at.openapi({ 49 | format: "date-time", 50 | example: "2024-03-10T09:55:07.213Z", 51 | }), 52 | }) 53 | .openapi("Station") satisfies typeof stationSchema 54 | -------------------------------------------------------------------------------- /src/sync/headers.ts: -------------------------------------------------------------------------------- 1 | export const KAI_HEADERS = { 2 | "User-Agent": 3 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0", 4 | Accept: "application/json, text/javascript, */*; q=0.01", 5 | "Accept-Language": "en-US,en;q=0.5", 6 | Authorization: 7 | "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", 8 | Priority: "u=0", 9 | } 10 | -------------------------------------------------------------------------------- /src/sync/schedule.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "bun" 2 | import { eq, sql } from "drizzle-orm" 3 | import { z } from "zod" 4 | import { 5 | NewSchedule, 6 | NewStation, 7 | scheduleTable, 8 | stationTable, 9 | } from "../db/schema" 10 | import { Database } from "../modules/v1/database" 11 | import { parseTime } from "../utils/time" 12 | import { KAI_HEADERS } from "./headers" 13 | 14 | const sync = async () => { 15 | if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL env is missing") 16 | if (!process.env.COMULINE_ENV) throw new Error("COMULINE_ENV env is missing") 17 | if (!process.env.KRL_ENDPOINT_BASE_URL) 18 | throw new Error("KRL_ENDPOINT_BASE_URL env is missing") 19 | 20 | const { db } = new Database({ 21 | COMULINE_ENV: process.env.COMULINE_ENV, 22 | DATABASE_URL: process.env.DATABASE_URL, 23 | }) 24 | 25 | const stations = await db 26 | .select({ 27 | id: stationTable.id, 28 | metadata: stationTable.metadata, 29 | name: stationTable.name, 30 | }) 31 | .from(stationTable) 32 | 33 | const batchSizes = 5 34 | const totalBatches = Math.ceil(stations.length / batchSizes) 35 | 36 | const schema = z.object({ 37 | status: z.number(), 38 | data: z.array( 39 | z.object({ 40 | train_id: z.string(), 41 | ka_name: z.string(), 42 | route_name: z.string(), 43 | dest: z.string(), 44 | time_est: z.string(), 45 | color: z.string(), 46 | dest_time: z.string(), 47 | }), 48 | ), 49 | }) 50 | 51 | for (let i = 0; i < totalBatches; i++) { 52 | const start = i * batchSizes 53 | const end = start + batchSizes 54 | const batch = stations.slice(start, end) 55 | 56 | await Promise.allSettled( 57 | batch.map(async ({ id, metadata }) => { 58 | await sleep(5000) 59 | 60 | const url = `${process.env.KRL_ENDPOINT_BASE_URL}/schedule?stationid=${id}&timefrom=00:00&timeto=23:00` 61 | 62 | console.info(`[SYNC][SCHEDULE][${id}] Send preflight`) 63 | const optionsResponse = await fetch(url, { 64 | method: "OPTIONS", 65 | headers: { 66 | ...KAI_HEADERS, 67 | "Access-Control-Request-Method": "GET", 68 | "Access-Control-Request-Headers": "authorization,content-type", 69 | }, 70 | credentials: "include", 71 | mode: "cors", 72 | }) 73 | 74 | if (!optionsResponse.ok) { 75 | throw new Error( 76 | `OPTIONS request failed with status: ${optionsResponse.status}`, 77 | ) 78 | } 79 | const req = await fetch(url, { 80 | method: "GET", 81 | headers: KAI_HEADERS, 82 | credentials: "include", 83 | mode: "cors", 84 | }) 85 | 86 | console.info(`[SYNC][SCHEDULE][${id}] Fetched data from API`) 87 | 88 | if (req.status === 200) { 89 | try { 90 | const data = await req.json() 91 | 92 | const parsed = schema.safeParse(data) 93 | 94 | if (!parsed.success) { 95 | console.error(`[SYNC][SCHEDULE][${id}] Error parse`) 96 | } else { 97 | const values = parsed.data.data.map((d) => { 98 | let [origin, destination] = d.route_name.split("-") 99 | 100 | const fixName = (name: string) => { 101 | switch (name) { 102 | case "TANJUNGPRIUK": 103 | return "TANJUNG PRIOK" 104 | case "JAKARTAKOTA": 105 | return "JAKARTA KOTA" 106 | case "KAMPUNGBANDAN": 107 | return "KAMPUNG BANDAN" 108 | case "TANAHABANG": 109 | return "TANAH ABANG" 110 | case "PARUNGPANJANG": 111 | return "PARUNG PANJANG" 112 | case "BANDARASOEKARNOHATTA": 113 | return "BANDARA SOEKARNO HATTA" 114 | default: 115 | return name 116 | } 117 | } 118 | 119 | origin = fixName(origin) 120 | destination = fixName(destination) 121 | 122 | return { 123 | id: `sc_krl_${id}_${d.train_id}`.toLowerCase(), 124 | station_id: id, 125 | station_origin_id: stations.find( 126 | ({ name }) => name === origin, 127 | )?.id!, 128 | station_destination_id: stations.find( 129 | ({ name }) => name === destination, 130 | )?.id!, 131 | train_id: d.train_id, 132 | line: d.ka_name, 133 | route: d.route_name, 134 | departs_at: parseTime(d.time_est).toISOString(), 135 | arrives_at: parseTime(d.dest_time).toISOString(), 136 | metadata: { 137 | origin: { 138 | color: d.color, 139 | }, 140 | }, 141 | } satisfies NewSchedule 142 | }) 143 | 144 | const insert = await db 145 | .insert(scheduleTable) 146 | .values(values) 147 | .onConflictDoUpdate({ 148 | target: scheduleTable.id, 149 | set: { 150 | departs_at: sql`excluded.departs_at`, 151 | arrives_at: sql`excluded.arrives_at`, 152 | metadata: sql`excluded.metadata`, 153 | updated_at: new Date().toISOString(), 154 | }, 155 | }) 156 | .returning() 157 | 158 | console.info( 159 | `[SYNC][SCHEDULE][${id}] Inserted ${insert.length} rows`, 160 | ) 161 | } 162 | } catch (err) { 163 | console.error( 164 | `[SYNC][SCHEDULE][${id}] Error inserting schedule data. Trace: ${JSON.stringify( 165 | err, 166 | )}. Status: ${req.status}.`, 167 | ) 168 | } 169 | } else if (req.status === 404) { 170 | console.info(`[SYNC][SCHEDULE][${id}] No schedule data found`) 171 | const payload: Partial = { 172 | metadata: metadata 173 | ? { 174 | ...metadata, 175 | active: false, 176 | } 177 | : null, 178 | updated_at: new Date().toISOString(), 179 | } 180 | await db 181 | .update(stationTable) 182 | .set(payload) 183 | .where(eq(scheduleTable.id, id)) 184 | console.info( 185 | `[SYNC][SCHEDULE][${id}] Updated station schedule availability status`, 186 | ) 187 | } else { 188 | const err = await req.json() 189 | const txt = await req.text() 190 | console.error( 191 | `[SYNC][SCHEDULE][${id}] Error fetch schedule data. Trace: ${JSON.stringify( 192 | err, 193 | )}. Status: ${req.status}. Req: ${txt}`, 194 | ) 195 | throw new Error(JSON.stringify(err)) 196 | } 197 | }), 198 | ) 199 | } 200 | } 201 | 202 | sync() 203 | -------------------------------------------------------------------------------- /src/sync/station.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm" 2 | import { z } from "zod" 3 | import { NewStation, stationTable, StationType } from "../db/schema" 4 | import { Database } from "../modules/v1/database" 5 | import { KAI_HEADERS } from "./headers" 6 | 7 | const createStationKey = (type: StationType, id: string) => 8 | `st_${type}_${id}`.toLocaleLowerCase() 9 | 10 | const sync = async () => { 11 | if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL env is missing") 12 | if (!process.env.COMULINE_ENV) throw new Error("COMULINE_ENV env is missing") 13 | if (!process.env.KRL_ENDPOINT_BASE_URL) 14 | throw new Error("KRL_ENDPOINT_BASE_URL env is missing") 15 | 16 | const { db } = new Database({ 17 | COMULINE_ENV: process.env.COMULINE_ENV, 18 | DATABASE_URL: process.env.DATABASE_URL, 19 | }) 20 | 21 | const schema = z.object({ 22 | status: z.number(), 23 | message: z.string(), 24 | data: z.array( 25 | z.object({ 26 | sta_id: z.string(), 27 | sta_name: z.string(), 28 | group_wil: z.number(), 29 | fg_enable: z.number(), 30 | }), 31 | ), 32 | }) 33 | 34 | const url = `${process.env.KRL_ENDPOINT_BASE_URL}/krl-station` 35 | 36 | const req = await fetch(url, { 37 | method: "GET", 38 | headers: KAI_HEADERS, 39 | }) 40 | 41 | if (!req.ok) 42 | throw new Error( 43 | `[SYNC][STATION] Request failed with status: ${req.status}`, 44 | { 45 | cause: await req.text(), 46 | }, 47 | ) 48 | 49 | const data = await req.json() 50 | 51 | const parsedData = schema.safeParse(data) 52 | 53 | if (!parsedData.success) { 54 | throw new Error(parsedData.error.message, { 55 | cause: parsedData.error.cause, 56 | }) 57 | } 58 | 59 | const filteredStation = parsedData.data.data.filter( 60 | (d) => !d.sta_id.includes("WIL"), 61 | ) 62 | 63 | const stations = filteredStation.map((s) => { 64 | return { 65 | uid: createStationKey("KRL", s.sta_id), 66 | id: s.sta_id, 67 | name: s.sta_name, 68 | type: "KRL", 69 | metadata: { 70 | active: true, 71 | origin: { 72 | fg_enable: s.fg_enable, 73 | daop: s.group_wil === 0 ? 1 : s.group_wil, 74 | }, 75 | }, 76 | } 77 | }) satisfies NewStation[] 78 | 79 | const newStations = [ 80 | /** Bandara Soekarno Hatta */ 81 | { 82 | uid: createStationKey("KRL", "BST"), 83 | id: "BST", 84 | name: "BANDARA SOEKARNO HATTA", 85 | type: "KRL", 86 | metadata: { 87 | active: true, 88 | origin: { 89 | fg_enable: 1, 90 | daop: 1, 91 | }, 92 | }, 93 | }, 94 | /** Cikampek */ 95 | { 96 | uid: createStationKey("KRL", "CKP"), 97 | id: "CKP", 98 | name: "CIKAMPEK", 99 | type: "LOCAL", 100 | metadata: { 101 | active: true, 102 | origin: { 103 | fg_enable: 1, 104 | daop: 1, 105 | }, 106 | }, 107 | }, 108 | /** Purwakarta */ 109 | { 110 | uid: createStationKey("KRL", "PWK"), 111 | id: "PWK", 112 | name: "PURWAKARTA", 113 | type: "LOCAL", 114 | metadata: { 115 | active: true, 116 | origin: { 117 | fg_enable: 1, 118 | daop: 2, 119 | }, 120 | }, 121 | }, 122 | ] satisfies NewStation[] 123 | 124 | const insertStations = [...newStations, ...stations] 125 | 126 | await db 127 | .insert(stationTable) 128 | .values(insertStations) 129 | .onConflictDoUpdate({ 130 | target: stationTable.uid, 131 | set: { 132 | updated_at: new Date().toISOString(), 133 | uid: sql`excluded.uid`, 134 | id: sql`excluded.id`, 135 | name: sql`excluded.name`, 136 | }, 137 | }) 138 | .returning() 139 | 140 | console.info(`[SYNC][STATION] Inserted ${insertStations.length} rows`) 141 | 142 | process.exit(0) 143 | } 144 | 145 | sync() 146 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "./modules/v1/database" 2 | import { constructResponse } from "./utils/response" 3 | 4 | export type Bindings = { 5 | DATABASE_URL: string 6 | COMULINE_ENV: string 7 | UPSTASH_REDIS_REST_TOKEN: string 8 | UPSTASH_REDIS_REST_URL: string 9 | } 10 | 11 | export type Variables = { 12 | db: Database["db"] 13 | constructResponse: typeof constructResponse 14 | } 15 | 16 | export type Environments = { 17 | Bindings: Bindings 18 | Variables: Variables 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/response.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig } from "@hono/zod-openapi" 2 | import { HTTPException } from "hono/http-exception" 3 | import { type StatusCode } from "hono/utils/http-status" 4 | import { z } from "zod" 5 | 6 | export const constructResponse = ( 7 | schema: T, 8 | data: z.infer, 9 | ): z.infer => { 10 | const result = schema.safeParse(data) 11 | 12 | if (!result.success) { 13 | console.log(result.error.issues) 14 | throw new HTTPException(417, { 15 | message: "Failed to construct a response", 16 | cause: result.error.issues, 17 | }) 18 | } 19 | 20 | return result.data 21 | } 22 | 23 | interface BaseResponseSchema { 24 | status: number 25 | } 26 | 27 | interface DataResponseSchema extends BaseResponseSchema { 28 | type: "data" 29 | schema: z.ZodTypeAny 30 | } 31 | 32 | interface MetadataResponseSchema extends BaseResponseSchema { 33 | type: "metadata" 34 | description?: string 35 | } 36 | 37 | export const buildResponseSchemas = ( 38 | responses: Array, 39 | ): RouteConfig["responses"] => { 40 | let result: RouteConfig["responses"] = {} 41 | 42 | for (const { status, ...rest } of responses) { 43 | if (rest.type === "data") { 44 | const { schema } = rest 45 | result[status] = { 46 | content: { 47 | "application/json": { 48 | schema: z.object({ 49 | metadata: z.object({ 50 | success: z.boolean().default(true), 51 | }), 52 | data: schema, 53 | }), 54 | }, 55 | }, 56 | description: "Success", 57 | } satisfies RouteConfig["responses"][string] 58 | } else { 59 | const { description } = rest 60 | 61 | const defaultDescription = 62 | description ?? getDefaultDescription(status as StatusCode) 63 | 64 | result[status] = { 65 | content: { 66 | "application/json": { 67 | schema: z.object({ 68 | metadata: z.object({ 69 | success: z.boolean().default(false), 70 | message: z.string().min(1).default(defaultDescription), 71 | }), 72 | }), 73 | }, 74 | }, 75 | description: defaultDescription, 76 | } satisfies RouteConfig["responses"][string] 77 | } 78 | } 79 | 80 | return result 81 | } 82 | 83 | const getDefaultDescription = (status: StatusCode) => { 84 | switch (status) { 85 | case 404: 86 | return "Not found" 87 | default: 88 | return "Internal server error" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | export function getSecsToMidnight(): number { 2 | const now = new Date() 3 | const tomorrow = new Date(now) 4 | tomorrow.setHours(0, 0, 0, 0) 5 | tomorrow.setDate(tomorrow.getDate() + 1) 6 | 7 | return Math.floor((tomorrow.getTime() - now.getTime()) / 1000) 8 | } 9 | 10 | export function parseTime(timeString: string): Date { 11 | const [hours, minutes, seconds] = timeString.split(":").map(Number) 12 | 13 | // Create date object 14 | const date = new Date() 15 | 16 | // Get the timezone offset in minutes (GMT+7 = -420 minutes) 17 | const targetOffset = -420 // GMT+7 in minutes 18 | const currentOffset = date.getTimezoneOffset() 19 | 20 | // Calculate the difference in offset 21 | const offsetDiff = targetOffset - currentOffset 22 | 23 | // Set time components and adjust for timezone 24 | date.setHours( 25 | hours ?? date.getHours(), 26 | (minutes ?? date.getMinutes()) + offsetDiff, 27 | seconds ?? date.getSeconds(), 28 | ) 29 | 30 | return date 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ES2022" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": [ 36 | "bun-types" 37 | ] /* Specify type package names to be included without being referenced in a source file. */, 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "resolveJsonModule": true, /* Enable importing .json files. */ 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | 43 | /* JavaScript Support */ 44 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 45 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 46 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 47 | 48 | /* Emit */ 49 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 50 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 51 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 52 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 53 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 54 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | // "noEmit": true, /* Disable emitting files from a compilation. */ 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 72 | 73 | /* Interop Constraints */ 74 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 75 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 76 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 79 | 80 | /* Type Checking */ 81 | "strict": true /* Enable all strict type-checking options. */, 82 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 83 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 86 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 87 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 88 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 89 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 90 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 91 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 92 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 93 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 94 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 95 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 96 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 97 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 98 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 99 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 100 | 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 104 | "baseUrl": "./src", 105 | "paths": { 106 | "@/*": ["*"] 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup" 2 | 3 | export default defineConfig((options: Options) => ({ 4 | entry: ["src/index.ts"], 5 | format: ["esm"], 6 | minify: true, 7 | outDir: ".dist", 8 | clean: true, 9 | metafile: true, 10 | ...options, 11 | })) 12 | -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/wrangler/config-schema.json", 3 | "name": "comuline-api", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-03-05", 6 | "send_metrics": false, 7 | "minify": true, 8 | "limits": { 9 | "cpu_ms": 300000, 10 | }, 11 | "dev": { 12 | "port": 3001, 13 | }, 14 | "observability": { 15 | "enabled": true, 16 | }, 17 | "placement": { 18 | "mode": "smart", 19 | }, 20 | } 21 | --------------------------------------------------------------------------------