From 8eacb68a7e0a41ae4b1026b99bf4531539f100ea Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Thu, 15 May 2025 21:50:11 +0200 Subject: [PATCH] =?UTF-8?q?Dodaj=20mikroserwis=20Redis=20API=20z=20obs?= =?UTF-8?q?=C5=82ug=C4=85=20Docker=20i=20Bun=20(Migracja=20api=20do=20oddz?= =?UTF-8?q?ielnego=20repozytorium)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Utworzono API w Bun i TypeScript do obsługi Redis (GET/POST/DELETE) z różnymi typami danych (string, list, set, hash). - Dodano `docker-compose.yaml` i `dockerfile` do uruchamiania Redis i API jako kontenery. - Skonfigurowano `.dockerignore` i `.gitignore` dla czystości repozytorium. - Użyto `bun.lockb` oraz `package.json` do zależności i blokady wersji. - Skonfigurowano `tsconfig.json` dla kompilatora TypeScript. --- .dockerignore | 4 + .gitignore | 35 ++++++++ bun.lockb | Bin 0 -> 6853 bytes docker-compose.yaml | 32 ++++++++ dockerfile | 27 ++++++ index.ts | 195 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 ++++ tsconfig.json | 27 ++++++ 8 files changed, 336 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100755 bun.lockb create mode 100644 docker-compose.yaml create mode 100644 dockerfile create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17aa17a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +docker-compose.yaml +data +tsconfig.json \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f1800a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +"$(pwd)" +slim.report.json +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + + +data + +commit.txt +git-diff.txt \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..f41bd17c27e5a52efd9fcd281ef7bfa74413be15 GIT binary patch literal 6853 zcmeHMdsI~A79YkT1r<@r2Z%z1*OWQTgNF#Bc#*m);ww}T6vs1gV3>h92M{cQltjfF zB;>2BBKX8-7Z)XYAw?2N!z>B&5lRu7k|=6wdiOqaK0UQcM!SEs?sB^h`+SezZ}0E5 z&o^5aXZIx5=suUxyVLq)r@2XbYk1^3gCQDrwQYLdeyy4DOhIcVdXWuErX38becjSO(o4^iT_l#1`~G(A_|%M(K16%SqCOb}eYE4-<3<=&n%i4cZ!XuTByP^3Mj1 z^~IpEf2dFoZG68^r9Q38hcvcbF6=h!`kY{{NO@+yOQgEu<9EvZ%1<3GEuXTyj`aw% z4B4a%VVJDEwpDvi+rQ!5_Vd?1VQV%=HlFQ&YS-*88rEq^&d71Lo^<1} z7hX9yUXgio!H?0$vwK~6DbGD@#o+xTeXIZX&bu{{0K1mn#ew>?}&=@ zdkgTR0T0WJ>uKw*GfN0wC&H6Da|lS^ z&G6za*hieXdx+rA!VAtH`i+kbl9(X`?*I!&<`40%W?-=k!N&rA1lYHOIymo7ZGSJ| z0|0LcSPW{Y%@Jb%I^aD3j}Z{D5X;OGf>*-E^Aqgj+>?59NWioNA3S%s_i_Fp7V>q( zkKfZ|oly4>a1eWWJO*s6huF;fg7b>y7vVucjpaUA5ah4n$AcaZ(6Rre0H1}##4Dzj zo%p$Uz-Y&QH4he7P5-bmzC0_Y=|PEOm)eafSIb)hcI@dtq5DazNfqIWd$)s{hHP8W zXZ^Y6harhh?d@&G8ix}}B3|)&%h+inZ=Rt4d5B`)c8qZwG->RQg|~_g^?pCE8+c~t zROXwHgSU^5{8lzz)oR!E`0F1H9vA-IXNzs9+%aqBj{HQu>#m3w*Mx|Saire7)ulDq z#x~$i=F*F$R;}mnf7L%B>)?`eTUrmjT+(zg?}FQP)&2Z6#|})o7a5wBvj1rQbgI7Y z#`58-$2R+mc*Sv27I^GtABWiyEgvjP8|dW`)<7&S6%4SYChkpmQ6s0eWw7k}A_wbw(ZX`0l;&Ufk^2(wS*8-Z8TD=$dZm-iu z_pblewNQ4W_N}Idn};puhgJ;juADi&E&sdV{M8rlPizY=$V$0;u-~<*RjSMN>la-U z@ru7mvJ0JqmYz12to-EEiN0=uA5C2OZeaJHRwTGOYn%_n7muxdcxl?!hL+ruXWtLd zQ?&8?JJLOi3f&Ht(bFoj=DaHZQN$~bld}EGI$4&-`pnp9tCl1xcLq&WCA{TQS-G*t zr+H`2b(hrVu9a52cRyHqOJ&&7(9M6)PAI_7zIr z{vV@HP`Ii zDB^`X+#``~DwvTyd6%zt!^wuzmZwAC8`|}SoGG08qa91_<5Z0$N9szo;hJSLF0>_T zsw2|x1Xjka)32}ERq%uN?80yUwzEpai{A($vaY-7imjP@jGwzk%{r__S+7K-O}sTt(vk~&Mvz%bRu37XOUaB zujX*`m&}||N!9a*1xTxNLk>*Y_vI+>Wpqqbj@{a`T=5~VIyT+Ego|;WtT(BDHw_BSunUb*Up?ac*{H_$WUj<} z+dk*-ClyRQ{<8bQ#j9tgTF!FMe&d68oo_c*jDJykIjva4i*cWbY}4UUnRBN4ZRJ8~ZhUr*P?gUTt?E`^|@eXSPT0@mhM7 zsofgx!hOAWKK;kvg&%*Y68J=aJ0LQYl5%?lbIb}}Kj=$py2Le}S@o>-2Eauxhs1$sV zW6}Wx&N}RQH9yr8BIe!2hP?w*%SW8Q;Gh2jdW=7c literal 0 HcmV?d00001 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3924269 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,32 @@ +name: redis-dev +services: + redis-stack: + image: redis/redis-stack:latest + container_name: redis-stack + restart: unless-stopped + ports: + - "6379:6379" + - "8001:8001" + networks: + - redis-net + volumes: + - redis-data:/data + redis-api: + image: gitea.garandplg.com/garands-world-game/gwg-redis-api:latest + container_name: redis-api + restart: unless-stopped + ports: + - "5001:5001" + networks: + - redis-net + depends_on: + - redis-stack + + +networks: + redis-net: + driver: bridge + + +volumes: + redis-data: diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..3757a6a --- /dev/null +++ b/dockerfile @@ -0,0 +1,27 @@ +FROM oven/bun:alpine + +LABEL maintainer="garandplg@garandplg.com" +LABEL version="0.0.10" +LABEL description="Mikroserwis API do obsługi Redis" + +WORKDIR /api + +COPY package.json bun.lockb ./ + +RUN bun install --production --frozen-lockfile \ + && bun pm cache rm \ + && rm -rf /root/.bun + +COPY . . + +RUN addgroup --system redis_api \ + && adduser --system --no-create-home --ingroup redis_api redis_api \ + && chown -R redis_api:redis_api /api + +USER redis_api + +ENV IS_DOCKER=true + +EXPOSE 5001 + +CMD ["bun", "run", "index.ts"] diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..eade0fe --- /dev/null +++ b/index.ts @@ -0,0 +1,195 @@ +import { serve } from 'bun'; +import { createClient } from 'redis'; + +const client = createClient({ + url: Bun.env.IS_DOCKER ? 'redis://redis-stack:6379' : 'redis://localhost:6379' +}); + +client.on('error', (err: Error) => console.error('Redis Client Error', err)); +await client.connect(); + +const healthCheck = async () => { + try { + await client.ping(); + return { status: 'OK' }; + } catch (error: unknown) { + return { + status: 'ERROR', + message: error instanceof Error ? error.message : 'Nieznany błąd' + }; + } +}; + +const handleRedisAction = async ( + method: string, + key?: string, + value?: any, + ttl?: number, + type: string = 'string' +) => { + switch (method) { + case 'GET': + if (!key) return { error: 'Brak klucza' }; + + const keyExists = await client.exists(key); + if (!keyExists) return { value: null, type: null }; + + const valueType = await client.type(key); + switch (valueType) { + case 'string': + const getValue = await client.get(key); + return { value: getValue ? JSON.parse(getValue) : null, type: valueType }; + + case 'list': + const getList = await client.lRange(key, 0, -1); + return { + value: getList ? getList.map(item => JSON.parse(item)) : [], + type: valueType + }; + + case 'hash': + const getHash = await client.hGetAll(key); + const parsedHash = Object.fromEntries( + Object.entries(getHash).map(([k, v]) => [k, JSON.parse(v)]) + ); + return { value: parsedHash || {}, type: valueType }; + + case 'set': + const getSet = await client.sMembers(key); + return { + value: getSet ? getSet.map(item => JSON.parse(item)) : [], + type: valueType + }; + + default: + return { error: 'Nieobsługiwany typ danych' }; + } + + case 'POST': + if (!key) return { error: 'Brak klucza' }; + + switch (type) { + case 'string': + if (!value) return { error: 'Brak wartości' }; + + await client.set(key, JSON.stringify(value)); + if (ttl && ttl > 0) await client.expire(key, ttl); + + return { message: 'Klucz string ustawiony' }; + + case 'hash': + if (!value || typeof value !== 'object') + return { error: 'Brak wartości dla hash lub niepoprawny typ' }; + + const stringifiedHash = Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, JSON.stringify(v)]) + ); + await client.hSet(key, stringifiedHash); + if (ttl && ttl > 0) await client.expire(key, ttl); + + return { message: 'Hash ustawiony' }; + + case 'list': + if (!Array.isArray(value)) return { error: 'Wartość musi być tablicą' }; + + const stringifiedList = value.map(item => JSON.stringify(item)); + await client.rPush(key, stringifiedList); + if (ttl && ttl > 0) await client.expire(key, ttl); + + return { message: 'Lista ustawiona' }; + + case 'set': + if (!Array.isArray(value)) return { error: 'Wartość musi być tablicą' }; + + const stringifiedSet = value.map(item => JSON.stringify(item)); + await client.sAdd(key, stringifiedSet); + if (ttl && ttl > 0) await client.expire(key, ttl); + + return { message: 'Set ustawiony' }; + + // case 'zset': + // if ( + // !Array.isArray(value) || + // !value.every((item: any) => item.score !== undefined && item.value !== undefined) + // ) + // return { error: 'Wartość musi być tablicą obiektów z polem "score" i "value"' }; + // for (const item of value) { + // await client.zAdd(key, { score: item.score, value: item.value }); + // } + // console.log('SET (zset)', key, value); + // return { message: 'Sorted Set ustawiony' }; + + default: + return { error: 'Nieobsługiwany typ danych' }; + } + + case 'DELETE': + if (!key) return { error: 'Brak klucza' }; + + if (key === 'FLUSHALL') { + await client.flushAll(); + return { message: 'Wszystkie klucze zostały usunięte' }; + } + + await client.del(key); + return { message: 'Klucz usunięty' }; + + default: + return { error: 'Nieobsługiwana metoda' }; + } +}; + +serve({ + port: 5001, + async fetch(req: Request) { + const url = new URL(req.url); + const method = req.method; + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type' + }; + + if (method === 'OPTIONS') { + return new Response('', { status: 200, headers }); + } + + if (url.pathname === '/api/redis') { + try { + const body = method !== 'GET' && method !== 'DELETE' ? await req.json() : null; + const key = + method === 'GET' || method === 'DELETE' ? url.searchParams.get('key') : body?.key; + const value = body?.value; + const ttl = body?.ttl ? parseInt(body.ttl, 10) : 0; + const type = body?.type; + + const result = await handleRedisAction(method, key, value, ttl, type); + + if (result?.error) + return new Response(JSON.stringify({ error: result.error }), { status: 400, headers }); + + return new Response(JSON.stringify(result), { status: 200, headers }); + } catch (error) { + return new Response( + JSON.stringify({ error: error instanceof Error ? error.message : 'Nieznany błąd' }), + { status: 400, headers } + ); + } + } else if (url.pathname === '/api/health') { + const health = await healthCheck(); + return new Response(JSON.stringify(health), { + status: health.status === 'OK' ? 200 : 500, + headers + }); + } + + return new Response(JSON.stringify({ error: 'Nieobsługiwany adres URL' }), { + status: 404, + headers + }); + } +}); + +console.log('Redis API uruchomiony na porcie 5001'); diff --git a/package.json b/package.json new file mode 100644 index 0000000..d5641dc --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "redis-api", + "version": "0.0.10", + "private": true, + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "redis": "^4.7.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}