diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..79ef292
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.json]
+insert_final_newline = ignore
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index cdbe43e..06d5be9 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,2 @@
ko_fi: sooluh
-custom: ["https://saweria.co/sooluh", "https://trakteer.id/sooluh"]
+custom: ['https://saweria.co/sooluh', 'https://trakteer.id/sooluh']
diff --git a/.gitignore b/.gitignore
index 63167b5..d308cf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
-node_modules/
-*.js
-.vercel/
+dist
+node_modules
+.vercel
+.yarn
+.vscode
+
+*.log
diff --git a/.gitpod.yml b/.gitpod.yml
index fccff0c..a612a9c 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -3,5 +3,5 @@ tasks:
command: yarn dev
ports:
- - port: 5000
+ - port: 3000
onOpen: ignore
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..3186f3f
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1 @@
+nodeLinker: node-modules
diff --git a/LICENSE b/LICENSE
index e6e59d8..2dbc4df 100644
--- a/LICENSE
+++ b/LICENSE
@@ -175,7 +175,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
END OF TERMS AND CONDITIONS
-Copyright 2019 Suluh Sulistiawan
+Copyright 2019 Abu Masyail
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 64dfa71..1190161 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,9 @@
-![kodepos](https://socialify.git.ci/sooluh/kodepos/image?description=1&descriptionEditable=Indonesian%20postal%20code%20search%20API%20by%20place%20name%2C%20village%20or%20city.&font=Raleway&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Ftwitter%2Ftwemoji%2Fmaster%2Fassets%2Fsvg%2F1f4ee.svg&name=1&owner=1&pattern=Charlie%20Brown&pulls=1&stargazers=1&theme=Dark)
+![@sooluh/kodepos](https://socialify.git.ci/sooluh/kodepos/image?description=1&descriptionEditable=Indonesian%20postal%20code%20search%20API%20by%20place%20name%2C%20village%20or%20city.&font=Raleway&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Ftwitter%2Ftwemoji%2Fmaster%2Fassets%2Fsvg%2F1f4ee.svg&name=1&owner=1&pattern=Charlie%20Brown&pulls=1&stargazers=1&theme=Dark)
+
+## Requirements
+
+- Node.js `>= 16.20.1`
+- Yarn `>= 1.22.0`
## Getting Started
@@ -8,7 +13,7 @@
git clone https://github.com/sooluh/kodepos.git
```
-2. Change the current directory to this repository folder
+2. Move to the repository directory
```bash
cd kodepos
@@ -20,7 +25,7 @@
yarn install
```
-4. Run the app! (locally)
+4. Run locally
- Development mode
@@ -46,12 +51,19 @@
The fastest way to use it privately on PaaS available
-[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsooluh%2Fkodepos%2Ftree%2Fmain)
-[![Deploy with Cyclic](https://ik.imagekit.io/sooluh/cyclic.svg)](https://app.cyclic.sh/#/join/sooluh)
+
+
+
+
+
+
+
+
+
## Basic Usage
-Base URL : [`http://localhost:5000`](https://kodepos.vercel.app)
+Base URL : [`http://localhost:3000`](https://kodepos.vercel.app)
| Endpoint | Description | Parameter | Method |
| ---------------------------------------------- | ------------------------------- | --------- | ------ |
@@ -62,43 +74,43 @@ Base URL : [`http://localhost:5000`](https://kodepos.vercel.app)
#### Request
-curl -XGET 'http://localhost:5000/search/?q=danasari'
+curl -XGET 'http://localhost:3000/search/?q=danasari'
#### Response
```json
{
- "success": true,
- "message": "Data search successfully parsed.",
+ "statusCode": 200,
+ "code": "OK",
"data": [
{
"province": "Jawa Tengah",
- "city": "Purbalingga",
- "subdistrict": "Karangjambu",
- "urban": "Danasari",
- "postalcode": "53357"
+ "regency": "Purbalingga",
+ "district": "Karangjambu",
+ "village": "Danasari",
+ "code": "53357"
},
{
"province": "Jawa Tengah",
- "city": "Tegal",
- "subdistrict": "Bojong",
- "urban": "Danasari",
- "postalcode": "52465"
+ "regency": "Tegal",
+ "district": "Bojong",
+ "village": "Danasari",
+ "code": "52465"
},
{
"province": "Jawa Tengah",
- "city": "Pemalang",
- "subdistrict": "Pemalang",
- "urban": "Danasari",
- "postalcode": "52314"
+ "regency": "Pemalang",
+ "district": "Pemalang",
+ "village": "Danasari",
+ "code": "52314"
},
{
"province": "Jawa Barat",
- "city": "Ciamis",
- "subdistrict": "Cisaga",
- "urban": "Danasari",
- "postalcode": "46386"
+ "regency": "Ciamis",
+ "district": "Cisaga",
+ "village": "Danasari",
+ "code": "46386"
}
]
}
@@ -110,6 +122,7 @@ List of awesome projects powered by this API
- [**kodepos-web**](https://github.com/dotslashf/kodepos-web)
Simple web-app for postcode search by [dotslashf](https://github.com/dotslashf)
+
- [**Kode POS**](https://github.com/AzharRivaldi/Kode-POS-Indonesia)
Indonesia postal code search application (kotlin) by [AzharRivaldi](https://github.com/AzharRivaldi)
@@ -117,9 +130,11 @@ List of awesome projects powered by this API
List of server APIs ready to use publicly
-- [https://kodepos.vercel.app](https://kodepos.vercel.app/?json=true)
-- [https://kodepos.cyclic.app](https://kodepos.cyclic.app/?json=true)
+- [https://kodepos.vercel.app](https://kodepos.vercel.app/?q=danasari) `latest`
+- [https://kodepos.cyclic.app](https://kodepos.cyclic.app/?q=danasari) `latest`
+- [https://kodepos.onrender.com](https://kodepos.onrender.com/?q=danasari) `latest`
+- [https://kodepos-82o09pkha-sooluh.vercel.app](https://kodepos-82o09pkha-sooluh.vercel.app/?q=danasari) `v2.2.0`
### License
-Code licensed under [Apache 2.0 License](https://github.com/sooluh/kodepos/blob/main/LICENSE).
+This project is licensed under [Apache 2.0 License](https://github.com/sooluh/kodepos/blob/main/LICENSE).
diff --git a/nodemon.json b/nodemon.json
index 5b96ae7..4a07792 100644
--- a/nodemon.json
+++ b/nodemon.json
@@ -1,17 +1,11 @@
{
"restartable": "rs",
- "ignore": [
- ".git",
- "node_modules/**/node_modules"
- ],
- "verbose": "true",
- "watch": [
- "src/"
- ],
+ "ignore": [".git", "node_modules/**/node_modules"],
+ "verbose": true,
+ "watch": ["src"],
"env": {
"NODE_ENV": "development"
},
- "legacyWatch": true,
"ext": "ts,json",
- "exec": "yarn ts-node"
+ "exec": "ts-node ./src/app.ts"
}
diff --git a/package.json b/package.json
index f03a25f..862890d 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,15 @@
{
"name": "@sooluh/kodepos",
- "version": "2.2.0",
+ "version": "3.0.0",
"description": "Indonesian postal code search API by place name, village or city",
- "main": "src/app.js",
+ "main": "dist/app.js",
"scripts": {
- "prebuild": "del-cli \"./src/*.js\"",
- "build": "tsc",
- "start": "node ./src/app.js",
- "ts-node": "ts-node ./src/app.ts",
+ "build": "tsc -p tsconfig.json",
+ "start": "node ./dist/app.js",
"dev": "nodemon",
- "postinstall": "yarn build"
+ "postinstall": "yarn build",
+ "format": "prettier --write .",
+ "commit": "git-cz"
},
"keywords": [
"carikodepos",
@@ -21,13 +21,34 @@
"code",
"kode-pos"
],
+ "prettier": {
+ "trailingComma": "es5",
+ "semi": false,
+ "singleQuote": true,
+ "useTabs": false,
+ "quoteProps": "consistent",
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "printWidth": 100
+ },
+ "config": {
+ "commitizen": {
+ "path": "cz-conventional-changelog"
+ }
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "yarn format && git add --all",
+ "prepare-commit-msg": "exec < /dev/tty && npx cz --hook || true"
+ }
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/sooluh/kodepos.git"
},
"author": {
- "name": "Suluh Sulistiawan",
- "email": "suluh.webdevelopers@hotmail.com",
+ "name": "Abu Masyail",
+ "email": "suluhs@aol.com",
"url": "https://suluh.my.id"
},
"license": "Apache-2.0",
@@ -36,23 +57,25 @@
},
"homepage": "https://github.com/sooluh/kodepos",
"dependencies": {
- "axios": "^0.21.1",
- "cheerio": "^1.0.0-rc.10",
- "fastify": "^3.19.0",
- "fastify-compress": "^3.6.0",
- "fastify-cors": "^6.0.2",
- "fastify-prettier": "^1.1.9",
- "header-generator": "^1.0.0",
- "qs": "^6.10.1"
+ "@fastify/compress": "^6.4.0",
+ "@fastify/cors": "^8.3.0",
+ "@fastify/etag": "^4.2.0",
+ "@fastify/rate-limit": "^8.0.3",
+ "axios": "^1.5.0",
+ "cheerio": "^1.0.0-rc.12",
+ "fastify": "^4.22.2",
+ "header-generator": "^2.1.39",
+ "round-robin-js": "^3.0.5"
},
"devDependencies": {
- "@types/cheerio": "^0.22.30",
- "@types/node": "^16.3.1",
- "@types/qs": "^6.9.7",
- "del-cli": "^4.0.1",
- "minify-all-js": "github:sProDev/minify-all-js",
- "nodemon": "^2.0.12",
- "ts-node": "^10.4.0",
- "typescript": "^4.5.4"
+ "@types/cheerio": "^0.22.32",
+ "@types/node": "^20.5.9",
+ "commitizen": "^4.3.0",
+ "cz-conventional-changelog": "^3.3.0",
+ "husky": "^8.0.3",
+ "nodemon": "^3.0.1",
+ "prettier": "^3.0.3",
+ "ts-node": "^10.9.1",
+ "typescript": "^5.2.2"
}
}
diff --git a/src/app.ts b/src/app.ts
index 81e5c29..60f237d 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,55 +1,58 @@
-import Fastify, { FastifyInstance } from 'fastify'
-import fastifyCompress from 'fastify-compress'
-import fastifyPrettier from 'fastify-prettier'
-import fastifyCors from 'fastify-cors'
-import { parse } from 'qs'
-
-import Routes from './routes'
-
-class App extends Routes {
- private readonly port: number = 5000
- private readonly server: FastifyInstance
-
- constructor() {
- super()
-
- if (typeof process.env.PORT !== 'undefined') {
- this.port = parseInt(process.env.PORT)
- }
-
- this.server = Fastify({
- ignoreTrailingSlash: true,
- caseSensitive: false,
- querystringParser: q => parse(q),
- logger: process.env.NODE_ENV === 'development'
- })
- }
-
- private async middleware(): Promise {
- await this.server.register(fastifyCors)
- await this.server.register(fastifyCompress)
- await this.server.register(fastifyPrettier, {
- alwaysOn: true
- })
- }
-
- public async start(): Promise {
- try {
- await this.middleware()
-
- this.server.setNotFoundHandler(this.override)
- this.server.setErrorHandler(this.error)
-
- this.routes(this.server)
-
- let address = await this.server.listen(this.port, '0.0.0.0')
- console.info('Listen to requests on', address)
- } catch (error) {
- console.error(error)
- process.exit(1)
- }
- }
+import fastify from 'fastify'
+import { sendNotFound } from './utils/spec'
+
+const app = async () => {
+ try {
+ console.info('Running app...')
+ const app = fastify({ ignoreTrailingSlash: true, caseSensitive: false })
+
+ await app.register(import('@fastify/cors'))
+ await app.register(import('@fastify/compress'))
+ await app.register(import('@fastify/etag'))
+ await app.register(import('@fastify/rate-limit'), { max: 2, timeWindow: '1 second' })
+ await app.register(import('./core'))
+
+ app.setNotFoundHandler((_request, reply) => {
+ return sendNotFound(reply)
+ })
+
+ app.setErrorHandler((error, _request, reply) => {
+ if (error.statusCode === 429) {
+ return reply.status(429).send({
+ statusCode: 429,
+ code: 'TOO_MANY_REQUESTS',
+ message: 'Request limit: 2x per second.',
+ })
+ }
+
+ return reply.status(500).send({
+ statusCode: 500,
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'Please contact the developer.',
+ })
+ })
+
+ if (process.env.NODE_ENV === 'production') {
+ for (const signal of ['SIGINT', 'SIGTERM']) {
+ process.on(signal, () => {
+ app.close().then((err) => {
+ console.log(`close application on ${signal}`)
+ process.exit(err ? 1 : 0)
+ })
+ })
+ }
+ }
+
+ const address = await app.listen({ host: '0.0.0.0', port: Number(process.env.PORT || 3000) })
+ console.info('Listen to requests on', address)
+ } catch (e) {
+ console.error(e)
+ }
}
-const app = new App()
-app.start()
+process.on('unhandledRejection', (e) => {
+ console.error(e)
+ process.exit(1)
+})
+
+app()
diff --git a/src/controller.ts b/src/controller.ts
deleted file mode 100644
index 13be73f..0000000
--- a/src/controller.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { FastifyReply, FastifyRequest } from 'fastify'
-import { DataResponse } from './types'
-import Kodepos from './kodepos'
-
-class Controller {
- public async home(request: FastifyRequest, reply: FastifyReply): Promise {
- const { homepage } = require(__dirname + '/../package.json')
- const baseurl = `${request.protocol}://${request.hostname}`
-
- // @ts-ignore
- let { q, json } = request.query
-
- if (typeof json !== 'undefined' && json.trim() != false) {
- const { author } = require(__dirname + '/../package.json')
-
- let response: DataResponse = {
- code: 200,
- status: true,
- messages: 'Welcome to kodepos! Read the API documentation on the listed github repository',
- data: {
- repository: `${homepage}#basic-usage`,
- example: `${baseurl}/search/?q=danasari`,
- author
- }
- }
-
- return reply.status(response.code).send(response)
- }
-
- if (typeof q !== 'undefined' && q.trim() !== '') {
- let redirect = `${baseurl}/search/?q=${q}`
- return reply.redirect(301, redirect)
- }
-
- return reply.redirect(302, homepage)
- }
-
- public async search(request: FastifyRequest, reply: FastifyReply): Promise {
- // @ts-ignore
- let { q } = request.query
-
- if (typeof q !== 'undefined' && q.trim() !== '') {
- let postal = new Kodepos(q)
- let response = await postal.search()
-
- return reply.status(response.code).send(response)
- }
-
- let response: DataResponse = {
- code: 400,
- status: false,
- messages: 'Cannot perform search without parameter "q"!'
- }
-
- return reply.status(response.code).send(response)
- }
-}
-
-export default Controller
diff --git a/src/controllers/home.ts b/src/controllers/home.ts
new file mode 100644
index 0000000..0ace00d
--- /dev/null
+++ b/src/controllers/home.ts
@@ -0,0 +1,15 @@
+import type { FastifyReply, FastifyRequest } from 'fastify'
+
+export const home = async (
+ request: FastifyRequest<{ Querystring: { q: string } }>,
+ reply: FastifyReply
+) => {
+ const { q } = request.query
+
+ if (typeof q !== 'undefined' && q.trim() !== '') {
+ const baseurl = `${request.protocol}://${request.hostname}`
+ return reply.redirect(301, `${baseurl}/search/?q=${q}`)
+ }
+
+ return reply.redirect(302, 'https://github.com/sooluh/kodepos')
+}
diff --git a/src/controllers/search.ts b/src/controllers/search.ts
new file mode 100644
index 0000000..1ae4adc
--- /dev/null
+++ b/src/controllers/search.ts
@@ -0,0 +1,19 @@
+import { search as scrape } from '../utils/kodepos'
+import { createSpecResponse, sendNotFound } from '../utils/spec'
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
+
+export const search = (app: FastifyInstance) => {
+ return async (request: FastifyRequest<{ Querystring: { q: string } }>, reply: FastifyReply) => {
+ const { q } = request.query
+
+ if (typeof q === 'undefined' || q.trim() === '') {
+ return sendNotFound(reply)
+ }
+
+ const provider = app.providers.next()
+ const result = await scrape({ query: q }, provider.value)
+ const response = createSpecResponse(result)
+
+ return reply.send(response)
+ }
+}
diff --git a/src/core.ts b/src/core.ts
new file mode 100644
index 0000000..bfe2e77
--- /dev/null
+++ b/src/core.ts
@@ -0,0 +1,17 @@
+import { routes } from './routes'
+import type { ProviderList } from './types'
+import { SequentialRoundRobin } from 'round-robin-js'
+import type { FastifyInstance, FastifyPluginOptions } from 'fastify'
+
+const load = async (app: FastifyInstance, _: FastifyPluginOptions) => {
+ const providers = new SequentialRoundRobin([
+ { hostname: 'direktorikodepos.org', segment: 'wilayah' },
+ { hostname: 'carikodepos.com', segment: 'daerah' },
+ ])
+
+ app.decorate('providers', providers)
+
+ routes(app)
+}
+
+export default load
diff --git a/src/kodepos.ts b/src/kodepos.ts
deleted file mode 100644
index f18a347..0000000
--- a/src/kodepos.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import HeaderGenerator from 'header-generator'
-import cheerio from 'cheerio'
-import axios from 'axios'
-import { DataResponse, DataResult, DataResults } from './types'
-
-class Kodepos {
- private readonly baseurl: string = 'https://direktorikodepos.org/'
- private readonly keywords: string
- private readonly headers: object
-
- constructor(keywords: string) {
- this.keywords = keywords
-
- this.headers = new HeaderGenerator({
- browsers: ['chrome', 'firefox', 'safari'],
- operatingSystems: ['linux', 'android', 'windows'],
- devices: ['desktop', 'mobile'],
- locales: ['id-ID']
- })
- }
-
- public async search(): Promise {
- const url = process.env.PROXY
- ? `${process.env.PROXY}/?${encodeURIComponent(this.baseurl + '?s=' + this.keywords)}`
- : this.baseurl + '?s=' + this.keywords
-
- try {
- let output = await axios({
- method: 'GET',
- url,
- headers: this.headers
- })
- const $: cheerio.Root = cheerio.load(output.data)
-
- let tr: cheerio.Cheerio = $('tr')
- if (tr.length > 0) {
- let results: DataResults = []
-
- tr.each((number: number, element: cheerio.Element): void => {
- if (number === 0) return
-
- let td: cheerio.Cheerio = $(element).find('td')
- let result: DataResult = {}
-
- td.each((index: number, html: cheerio.Element): void => {
- let value: string = $(html).find('a').text()
- let key: string = index === 0 ? 'province' :
- (index === 1 ? 'city' :
- (index === 2 ? 'subdistrict' :
- (index === 3 ? 'urban' : 'postalcode')))
-
- result[key] = value.trim()
- })
-
- if (Object.entries(result).length === 5) {
- results.push(result)
- }
- })
-
- let response: DataResponse = {
- code: 200,
- status: true,
- messages: 'Data search successfully parsed.',
- data: results
- }
-
- return response
- } else {
- let response: DataResponse = {
- code: 200,
- status: false,
- messages: 'No data can be returned.'
- }
-
- return response
- }
- } catch (error) {
- console.error(error)
-
- let response: DataResponse = {
- code: 500,
- status: false,
- messages: 'An error occurred in the script.'
- }
-
- return response
- }
- }
-}
-
-export default Kodepos
diff --git a/src/routes.ts b/src/routes.ts
index 95884b8..3a1d4a1 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -1,39 +1,8 @@
-import { FastifyError, FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
-import { DataResponse } from './types'
-import Controller from './controller'
+import { home } from './controllers/home'
+import { search } from './controllers/search'
+import type { FastifyInstance } from 'fastify'
-class Routes extends Controller {
- constructor() {
- super()
- }
-
- protected routes(app: FastifyInstance): void {
- app.get('/', this.home)
- app.get('/search', this.search)
- }
-
- protected async override(_request: FastifyRequest, reply: FastifyReply): Promise {
- let response: DataResponse = {
- code: 404,
- status: false,
- messages: 'Looks like the endpoint you\'re looking for can\'t be found.'
- }
-
- reply.status(response.code).send(response)
- }
-
- protected async error(error: FastifyError, _request: FastifyRequest, reply: FastifyReply): Promise {
- console.error(error)
-
- let response: DataResponse = {
- code: error.statusCode as number,
- status: false,
- messages: 'An error occurred either from the client or server.',
- error
- }
-
- reply.status(response.code).send(response)
- }
+export const routes = (app: FastifyInstance) => {
+ app.get('/', home)
+ app.get('/search', search(app))
}
-
-export default Routes
diff --git a/src/types.ts b/src/types.ts
index 7b1e15a..5d50052 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,21 +1,23 @@
-export interface DataResult {
- [key: string]: string
+export type KeywordOptions = {
+ query: string
+ province?: string
+ regency?: string
+ district?: string
}
-export interface DataResults extends Array