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