Dodaj mikroserwis Redis API z obsługą Docker i Bun (Migracja api do oddzielnego repozytorium)
- 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.
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
docker-compose.yaml
|
||||||
|
data
|
||||||
|
tsconfig.json
|
||||||
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@@ -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
|
||||||
32
docker-compose.yaml
Normal file
32
docker-compose.yaml
Normal file
@@ -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:
|
||||||
27
dockerfile
Normal file
27
dockerfile
Normal file
@@ -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"]
|
||||||
195
index.ts
Normal file
195
index.ts
Normal file
@@ -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');
|
||||||
16
package.json
Normal file
16
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user