landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
This commit is contained in:
+148
-1
@@ -8,7 +8,8 @@
|
||||
"name": "@landvex/infrastructure",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@landvex/domain": "file:../domain"
|
||||
"@landvex/domain": "file:../domain",
|
||||
"pg": "^8.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.0",
|
||||
@@ -3154,6 +3155,95 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
|
||||
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.14.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
"pg-protocol": "^1.15.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
|
||||
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
|
||||
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
|
||||
"integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -3197,6 +3287,45 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||
@@ -3406,6 +3535,15 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -3817,6 +3955,15 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@landvex/domain": "file:../domain"
|
||||
"@landvex/domain": "file:../domain",
|
||||
"pg": "^8.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.0",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* In-Memory API Key Repository
|
||||
*
|
||||
* For testing and development. Fast, isolated, no external dependencies.
|
||||
*/
|
||||
|
||||
import { ApiKey, ApiKeyId } from '../../../api/src/models/api-key';
|
||||
import { ApiKeyRepository, ApiKeyUsageRepository } from '../../repositories/api-key-repository';
|
||||
|
||||
export class InMemoryApiKeyRepository implements ApiKeyRepository {
|
||||
private keys = new Map<string, ApiKey>();
|
||||
|
||||
async save(apiKey: ApiKey): Promise<void> {
|
||||
this.keys.set(apiKey.id, apiKey);
|
||||
}
|
||||
|
||||
async findById(id: ApiKeyId): Promise<ApiKey | null> {
|
||||
return this.keys.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findByKeyHash(keyHash: string): Promise<ApiKey | null> {
|
||||
return Array.from(this.keys.values()).find(k => k.keyHash === keyHash) ?? null;
|
||||
}
|
||||
|
||||
async findAll(options?: { limit?: number; offset?: number; status?: string }): Promise<ApiKey[]> {
|
||||
let results = Array.from(this.keys.values());
|
||||
if (options?.status) {
|
||||
results = results.filter(k => k.status === options.status);
|
||||
}
|
||||
return results
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.slice(options?.offset ?? 0, (options?.offset ?? 0) + (options?.limit ?? 100));
|
||||
}
|
||||
|
||||
async count(options?: { status?: string }): Promise<number> {
|
||||
if (options?.status) {
|
||||
return Array.from(this.keys.values()).filter(k => k.status === options.status).length;
|
||||
}
|
||||
return this.keys.size;
|
||||
}
|
||||
|
||||
async update(apiKey: ApiKey): Promise<void> {
|
||||
this.keys.set(apiKey.id, apiKey);
|
||||
}
|
||||
|
||||
async delete(id: ApiKeyId): Promise<void> {
|
||||
this.keys.delete(id);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.keys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryApiKeyUsageRepository implements ApiKeyUsageRepository {
|
||||
private usages: Array<{
|
||||
apiKeyId: ApiKeyId;
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
clientIp?: string;
|
||||
}> = [];
|
||||
|
||||
async recordUsage(usage: {
|
||||
apiKeyId: ApiKeyId;
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
clientIp?: string;
|
||||
}): Promise<void> {
|
||||
this.usages.push(usage);
|
||||
}
|
||||
|
||||
async getUsageStats(apiKeyId: ApiKeyId, since: Date): Promise<{
|
||||
totalRequests: number;
|
||||
requestsPerMinute: number;
|
||||
requestsPerHour: number;
|
||||
requestsPerDay: number;
|
||||
averageResponseTimeMs: number;
|
||||
errorRate: number;
|
||||
}> {
|
||||
const relevant = this.usages.filter(u => u.apiKeyId === apiKeyId && u.timestamp >= since);
|
||||
const total = relevant.length;
|
||||
const errors = relevant.filter(u => u.statusCode >= 400).length;
|
||||
|
||||
return {
|
||||
totalRequests: total,
|
||||
requestsPerMinute: total, // Simplified for in-memory
|
||||
requestsPerHour: total,
|
||||
requestsPerDay: total,
|
||||
averageResponseTimeMs: total > 0
|
||||
? Math.round(relevant.reduce((sum, u) => sum + u.responseTimeMs, 0) / total)
|
||||
: 0,
|
||||
errorRate: total > 0 ? Math.round((errors / total) * 100) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
async getRecentUsage(apiKeyId: ApiKeyId, limit: number): Promise<Array<{
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
}>> {
|
||||
return this.usages
|
||||
.filter(u => u.apiKeyId === apiKeyId)
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
||||
.slice(0, limit)
|
||||
.map(u => ({
|
||||
timestamp: u.timestamp,
|
||||
endpoint: u.endpoint,
|
||||
method: u.method,
|
||||
statusCode: u.statusCode,
|
||||
responseTimeMs: u.responseTimeMs,
|
||||
}));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.usages = [];
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* PostgreSQL API Key Repository
|
||||
*
|
||||
* Production-ready adapter for API key persistence.
|
||||
* Uses pg client for database operations.
|
||||
*/
|
||||
import { ApiKey, ApiKeyId } from '../../../../api/src/models/api-key';
|
||||
import { ApiKeyRepository, ApiKeyUsageRepository } from '../../repositories/api-key-repository';
|
||||
export interface PostgresApiKeyConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
user: string;
|
||||
password: string;
|
||||
ssl?: boolean | object;
|
||||
}
|
||||
export declare class PostgresApiKeyRepository implements ApiKeyRepository {
|
||||
private pool;
|
||||
constructor(config: PostgresApiKeyConfig);
|
||||
init(): Promise<void>;
|
||||
save(apiKey: ApiKey): Promise<void>;
|
||||
findById(id: ApiKeyId): Promise<ApiKey | null>;
|
||||
findByKeyHash(keyHash: string): Promise<ApiKey | null>;
|
||||
findAll(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: string;
|
||||
}): Promise<ApiKey[]>;
|
||||
count(options?: {
|
||||
status?: string;
|
||||
}): Promise<number>;
|
||||
update(apiKey: ApiKey): Promise<void>;
|
||||
delete(id: ApiKeyId): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
export declare class PostgresApiKeyUsageRepository implements ApiKeyUsageRepository {
|
||||
private pool;
|
||||
constructor(config: PostgresApiKeyConfig);
|
||||
init(): Promise<void>;
|
||||
recordUsage(usage: {
|
||||
apiKeyId: ApiKeyId;
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
clientIp?: string;
|
||||
}): Promise<void>;
|
||||
getUsageStats(apiKeyId: ApiKeyId, since: Date): Promise<{
|
||||
totalRequests: number;
|
||||
requestsPerMinute: number;
|
||||
requestsPerHour: number;
|
||||
requestsPerDay: number;
|
||||
averageResponseTimeMs: number;
|
||||
errorRate: number;
|
||||
}>;
|
||||
getRecentUsage(apiKeyId: ApiKeyId, limit: number): Promise<Array<{
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
}>>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
//# sourceMappingURL=postgres-api-key-repository.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"postgres-api-key-repository.d.ts","sourceRoot":"","sources":["postgres-api-key-repository.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAe,MAAM,oCAAoC,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAEhG,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACxB;AAkCD,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,IAAI,CAAO;gBAEP,MAAM,EAAE,oBAAoB;IAIlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCrB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCnC,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAS9C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAStD,OAAO,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAqB1F,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAarD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAInC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,qBAAa,6BAA8B,YAAW,qBAAqB;IACzE,OAAO,CAAC,IAAI,CAAO;gBAEP,MAAM,EAAE,oBAAoB;IAIlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BrB,WAAW,CAAC,KAAK,EAAE;QACvB,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,EAAE,IAAI,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBX,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC;QAC5D,aAAa,EAAE,MAAM,CAAC;QACtB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAyBI,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QACrE,SAAS,EAAE,IAAI,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IAmBG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
||||
@@ -0,0 +1,254 @@
|
||||
"use strict";
|
||||
/**
|
||||
* PostgreSQL API Key Repository
|
||||
*
|
||||
* Production-ready adapter for API key persistence.
|
||||
* Uses pg client for database operations.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PostgresApiKeyUsageRepository = exports.PostgresApiKeyRepository = void 0;
|
||||
const pg_1 = require("pg");
|
||||
const api_key_1 = require("../../../../api/src/models/api-key");
|
||||
function createPool(config) {
|
||||
return new pg_1.Pool({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
database: config.database,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
ssl: config.ssl,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
}
|
||||
function rowToApiKey(row) {
|
||||
return new api_key_1.ApiKey({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
keyHash: row.key_hash,
|
||||
keyPrefix: row.key_prefix,
|
||||
status: row.status,
|
||||
rateLimitPerMinute: row.rate_limit_per_minute,
|
||||
rateLimitPerHour: row.rate_limit_per_hour,
|
||||
rateLimitPerDay: row.rate_limit_per_day,
|
||||
createdAt: row.created_at,
|
||||
expiresAt: row.expires_at,
|
||||
revokedAt: row.revoked_at,
|
||||
rotatedFromId: row.rotated_from_id,
|
||||
metadata: row.metadata ?? {},
|
||||
});
|
||||
}
|
||||
class PostgresApiKeyRepository {
|
||||
pool;
|
||||
constructor(config) {
|
||||
this.pool = createPool(config);
|
||||
}
|
||||
async init() {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
key_hash VARCHAR(255) NOT NULL UNIQUE,
|
||||
key_prefix VARCHAR(16) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
|
||||
rate_limit_per_minute INTEGER NOT NULL DEFAULT 60,
|
||||
rate_limit_per_hour INTEGER NOT NULL DEFAULT 1000,
|
||||
rate_limit_per_day INTEGER NOT NULL DEFAULT 10000,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
rotated_from_id UUID REFERENCES api_keys(id) ON DELETE SET NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
CONSTRAINT valid_rate_limits CHECK (
|
||||
rate_limit_per_minute > 0 AND
|
||||
rate_limit_per_hour >= rate_limit_per_minute AND
|
||||
rate_limit_per_day >= rate_limit_per_hour
|
||||
)
|
||||
)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_status ON api_keys(status)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_created_at ON api_keys(created_at DESC)
|
||||
`);
|
||||
}
|
||||
finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
async save(apiKey) {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(`INSERT INTO api_keys (
|
||||
id, name, key_hash, key_prefix, status,
|
||||
rate_limit_per_minute, rate_limit_per_hour, rate_limit_per_day,
|
||||
created_at, expires_at, revoked_at, rotated_from_id, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
status = EXCLUDED.status,
|
||||
revoked_at = EXCLUDED.revoked_at,
|
||||
metadata = EXCLUDED.metadata`, [
|
||||
apiKey.id,
|
||||
apiKey.name,
|
||||
apiKey.keyHash,
|
||||
apiKey.keyPrefix,
|
||||
apiKey.status,
|
||||
apiKey.rateLimitPerMinute,
|
||||
apiKey.rateLimitPerHour,
|
||||
apiKey.rateLimitPerDay,
|
||||
apiKey.createdAt,
|
||||
apiKey.expiresAt,
|
||||
apiKey.revokedAt,
|
||||
apiKey.rotatedFromId,
|
||||
JSON.stringify(apiKey.metadata),
|
||||
]);
|
||||
}
|
||||
finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
async findById(id) {
|
||||
const result = await this.pool.query('SELECT * FROM api_keys WHERE id = $1', [id]);
|
||||
if (result.rows.length === 0)
|
||||
return null;
|
||||
return rowToApiKey(result.rows[0]);
|
||||
}
|
||||
async findByKeyHash(keyHash) {
|
||||
const result = await this.pool.query('SELECT * FROM api_keys WHERE key_hash = $1', [keyHash]);
|
||||
if (result.rows.length === 0)
|
||||
return null;
|
||||
return rowToApiKey(result.rows[0]);
|
||||
}
|
||||
async findAll(options) {
|
||||
const limit = options?.limit ?? 100;
|
||||
const offset = options?.offset ?? 0;
|
||||
const status = options?.status;
|
||||
let query = 'SELECT * FROM api_keys';
|
||||
const params = [];
|
||||
if (status) {
|
||||
query += ' WHERE status = $1';
|
||||
params.push(status);
|
||||
}
|
||||
query += ' ORDER BY created_at DESC';
|
||||
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
||||
params.push(limit, offset);
|
||||
const result = await this.pool.query(query, params);
|
||||
return result.rows.map(rowToApiKey);
|
||||
}
|
||||
async count(options) {
|
||||
let query = 'SELECT COUNT(*) FROM api_keys';
|
||||
const params = [];
|
||||
if (options?.status) {
|
||||
query += ' WHERE status = $1';
|
||||
params.push(options.status);
|
||||
}
|
||||
const result = await this.pool.query(query, params);
|
||||
return parseInt(result.rows[0].count, 10);
|
||||
}
|
||||
async update(apiKey) {
|
||||
await this.save(apiKey);
|
||||
}
|
||||
async delete(id) {
|
||||
await this.pool.query('DELETE FROM api_keys WHERE id = $1', [id]);
|
||||
}
|
||||
async close() {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
exports.PostgresApiKeyRepository = PostgresApiKeyRepository;
|
||||
class PostgresApiKeyUsageRepository {
|
||||
pool;
|
||||
constructor(config) {
|
||||
this.pool = createPool(config);
|
||||
}
|
||||
async init() {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_key_usage (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
api_key_id UUID NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
endpoint VARCHAR(512) NOT NULL,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
status_code INTEGER NOT NULL,
|
||||
response_time_ms INTEGER NOT NULL,
|
||||
client_ip INET
|
||||
)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_key_usage_api_key_id ON api_key_usage(api_key_id)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_key_usage_timestamp ON api_key_usage(timestamp DESC)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_key_usage_api_key_timestamp
|
||||
ON api_key_usage(api_key_id, timestamp DESC)
|
||||
`);
|
||||
}
|
||||
finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
async recordUsage(usage) {
|
||||
await this.pool.query(`INSERT INTO api_key_usage (
|
||||
api_key_id, timestamp, endpoint, method, status_code, response_time_ms, client_ip
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
|
||||
usage.apiKeyId,
|
||||
usage.timestamp,
|
||||
usage.endpoint,
|
||||
usage.method,
|
||||
usage.statusCode,
|
||||
usage.responseTimeMs,
|
||||
usage.clientIp ?? null,
|
||||
]);
|
||||
}
|
||||
async getUsageStats(apiKeyId, since) {
|
||||
const result = await this.pool.query(`SELECT
|
||||
COUNT(*) as total_requests,
|
||||
AVG(response_time_ms) as avg_response_time,
|
||||
SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END)::FLOAT / NULLIF(COUNT(*), 0) as error_rate,
|
||||
COUNT(*) FILTER (WHERE timestamp >= NOW() - INTERVAL '1 minute') as requests_per_minute,
|
||||
COUNT(*) FILTER (WHERE timestamp >= NOW() - INTERVAL '1 hour') as requests_per_hour,
|
||||
COUNT(*) FILTER (WHERE timestamp >= NOW() - INTERVAL '1 day') as requests_per_day
|
||||
FROM api_key_usage
|
||||
WHERE api_key_id = $1 AND timestamp >= $2`, [apiKeyId, since]);
|
||||
const row = result.rows[0];
|
||||
return {
|
||||
totalRequests: parseInt(row.total_requests, 10),
|
||||
requestsPerMinute: parseInt(row.requests_per_minute, 10),
|
||||
requestsPerHour: parseInt(row.requests_per_hour, 10),
|
||||
requestsPerDay: parseInt(row.requests_per_day, 10),
|
||||
averageResponseTimeMs: Math.round(parseFloat(row.avg_response_time ?? '0')),
|
||||
errorRate: Math.round((parseFloat(row.error_rate ?? '0') * 100)),
|
||||
};
|
||||
}
|
||||
async getRecentUsage(apiKeyId, limit) {
|
||||
const result = await this.pool.query(`SELECT timestamp, endpoint, method, status_code, response_time_ms
|
||||
FROM api_key_usage
|
||||
WHERE api_key_id = $1
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT $2`, [apiKeyId, limit]);
|
||||
return result.rows.map(row => ({
|
||||
timestamp: row.timestamp,
|
||||
endpoint: row.endpoint,
|
||||
method: row.method,
|
||||
statusCode: row.status_code,
|
||||
responseTimeMs: row.response_time_ms,
|
||||
}));
|
||||
}
|
||||
async close() {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
exports.PostgresApiKeyUsageRepository = PostgresApiKeyUsageRepository;
|
||||
//# sourceMappingURL=postgres-api-key-repository.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* PostgreSQL API Key Repository
|
||||
*
|
||||
* Production-ready adapter for API key persistence.
|
||||
* Uses pg client for database operations.
|
||||
*/
|
||||
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { ApiKey, ApiKeyId, ApiKeyProps } from '../../../../api/src/models/api-key';
|
||||
import { ApiKeyRepository, ApiKeyUsageRepository } from '../../repositories/api-key-repository';
|
||||
|
||||
export interface PostgresApiKeyConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
user: string;
|
||||
password: string;
|
||||
ssl?: boolean | object;
|
||||
}
|
||||
|
||||
function createPool(config: PostgresApiKeyConfig): Pool {
|
||||
return new Pool({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
database: config.database,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
ssl: config.ssl,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
function rowToApiKey(row: Record<string, unknown>): ApiKey {
|
||||
return new ApiKey({
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
keyHash: row.key_hash as string,
|
||||
keyPrefix: row.key_prefix as string,
|
||||
status: row.status as ApiKeyProps['status'],
|
||||
rateLimitPerMinute: row.rate_limit_per_minute as number,
|
||||
rateLimitPerHour: row.rate_limit_per_hour as number,
|
||||
rateLimitPerDay: row.rate_limit_per_day as number,
|
||||
createdAt: row.created_at as Date,
|
||||
expiresAt: row.expires_at as Date | null,
|
||||
revokedAt: row.revoked_at as Date | null,
|
||||
rotatedFromId: row.rotated_from_id as string | null,
|
||||
metadata: (row.metadata as Record<string, unknown>) ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
export class PostgresApiKeyRepository implements ApiKeyRepository {
|
||||
private pool: Pool;
|
||||
|
||||
constructor(config: PostgresApiKeyConfig) {
|
||||
this.pool = createPool(config);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
key_hash VARCHAR(255) NOT NULL UNIQUE,
|
||||
key_prefix VARCHAR(16) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
|
||||
rate_limit_per_minute INTEGER NOT NULL DEFAULT 60,
|
||||
rate_limit_per_hour INTEGER NOT NULL DEFAULT 1000,
|
||||
rate_limit_per_day INTEGER NOT NULL DEFAULT 10000,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
rotated_from_id UUID REFERENCES api_keys(id) ON DELETE SET NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
CONSTRAINT valid_rate_limits CHECK (
|
||||
rate_limit_per_minute > 0 AND
|
||||
rate_limit_per_hour >= rate_limit_per_minute AND
|
||||
rate_limit_per_day >= rate_limit_per_hour
|
||||
)
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_status ON api_keys(status)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_created_at ON api_keys(created_at DESC)
|
||||
`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async save(apiKey: ApiKey): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(
|
||||
`INSERT INTO api_keys (
|
||||
id, name, key_hash, key_prefix, status,
|
||||
rate_limit_per_minute, rate_limit_per_hour, rate_limit_per_day,
|
||||
created_at, expires_at, revoked_at, rotated_from_id, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
status = EXCLUDED.status,
|
||||
revoked_at = EXCLUDED.revoked_at,
|
||||
metadata = EXCLUDED.metadata`,
|
||||
[
|
||||
apiKey.id,
|
||||
apiKey.name,
|
||||
apiKey.keyHash,
|
||||
apiKey.keyPrefix,
|
||||
apiKey.status,
|
||||
apiKey.rateLimitPerMinute,
|
||||
apiKey.rateLimitPerHour,
|
||||
apiKey.rateLimitPerDay,
|
||||
apiKey.createdAt,
|
||||
apiKey.expiresAt,
|
||||
apiKey.revokedAt,
|
||||
apiKey.rotatedFromId,
|
||||
JSON.stringify(apiKey.metadata),
|
||||
]
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: ApiKeyId): Promise<ApiKey | null> {
|
||||
const result = await this.pool.query(
|
||||
'SELECT * FROM api_keys WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
return rowToApiKey(result.rows[0]);
|
||||
}
|
||||
|
||||
async findByKeyHash(keyHash: string): Promise<ApiKey | null> {
|
||||
const result = await this.pool.query(
|
||||
'SELECT * FROM api_keys WHERE key_hash = $1',
|
||||
[keyHash]
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
return rowToApiKey(result.rows[0]);
|
||||
}
|
||||
|
||||
async findAll(options?: { limit?: number; offset?: number; status?: string }): Promise<ApiKey[]> {
|
||||
const limit = options?.limit ?? 100;
|
||||
const offset = options?.offset ?? 0;
|
||||
const status = options?.status;
|
||||
|
||||
let query = 'SELECT * FROM api_keys';
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (status) {
|
||||
query += ' WHERE status = $1';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at DESC';
|
||||
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
||||
params.push(limit, offset);
|
||||
|
||||
const result = await this.pool.query(query, params);
|
||||
return result.rows.map(rowToApiKey);
|
||||
}
|
||||
|
||||
async count(options?: { status?: string }): Promise<number> {
|
||||
let query = 'SELECT COUNT(*) FROM api_keys';
|
||||
const params: string[] = [];
|
||||
|
||||
if (options?.status) {
|
||||
query += ' WHERE status = $1';
|
||||
params.push(options.status);
|
||||
}
|
||||
|
||||
const result = await this.pool.query(query, params);
|
||||
return parseInt(result.rows[0].count, 10);
|
||||
}
|
||||
|
||||
async update(apiKey: ApiKey): Promise<void> {
|
||||
await this.save(apiKey);
|
||||
}
|
||||
|
||||
async delete(id: ApiKeyId): Promise<void> {
|
||||
await this.pool.query('DELETE FROM api_keys WHERE id = $1', [id]);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresApiKeyUsageRepository implements ApiKeyUsageRepository {
|
||||
private pool: Pool;
|
||||
|
||||
constructor(config: PostgresApiKeyConfig) {
|
||||
this.pool = createPool(config);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_key_usage (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
api_key_id UUID NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
endpoint VARCHAR(512) NOT NULL,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
status_code INTEGER NOT NULL,
|
||||
response_time_ms INTEGER NOT NULL,
|
||||
client_ip INET
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_key_usage_api_key_id ON api_key_usage(api_key_id)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_key_usage_timestamp ON api_key_usage(timestamp DESC)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_api_key_usage_api_key_timestamp
|
||||
ON api_key_usage(api_key_id, timestamp DESC)
|
||||
`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async recordUsage(usage: {
|
||||
apiKeyId: ApiKeyId;
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
clientIp?: string;
|
||||
}): Promise<void> {
|
||||
await this.pool.query(
|
||||
`INSERT INTO api_key_usage (
|
||||
api_key_id, timestamp, endpoint, method, status_code, response_time_ms, client_ip
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
usage.apiKeyId,
|
||||
usage.timestamp,
|
||||
usage.endpoint,
|
||||
usage.method,
|
||||
usage.statusCode,
|
||||
usage.responseTimeMs,
|
||||
usage.clientIp ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async getUsageStats(apiKeyId: ApiKeyId, since: Date): Promise<{
|
||||
totalRequests: number;
|
||||
requestsPerMinute: number;
|
||||
requestsPerHour: number;
|
||||
requestsPerDay: number;
|
||||
averageResponseTimeMs: number;
|
||||
errorRate: number;
|
||||
}> {
|
||||
const result = await this.pool.query(
|
||||
`SELECT
|
||||
COUNT(*) as total_requests,
|
||||
AVG(response_time_ms) as avg_response_time,
|
||||
SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END)::FLOAT / NULLIF(COUNT(*), 0) as error_rate,
|
||||
COUNT(*) FILTER (WHERE timestamp >= NOW() - INTERVAL '1 minute') as requests_per_minute,
|
||||
COUNT(*) FILTER (WHERE timestamp >= NOW() - INTERVAL '1 hour') as requests_per_hour,
|
||||
COUNT(*) FILTER (WHERE timestamp >= NOW() - INTERVAL '1 day') as requests_per_day
|
||||
FROM api_key_usage
|
||||
WHERE api_key_id = $1 AND timestamp >= $2`,
|
||||
[apiKeyId, since]
|
||||
);
|
||||
|
||||
const row = result.rows[0];
|
||||
return {
|
||||
totalRequests: parseInt(row.total_requests, 10),
|
||||
requestsPerMinute: parseInt(row.requests_per_minute, 10),
|
||||
requestsPerHour: parseInt(row.requests_per_hour, 10),
|
||||
requestsPerDay: parseInt(row.requests_per_day, 10),
|
||||
averageResponseTimeMs: Math.round(parseFloat(row.avg_response_time ?? '0')),
|
||||
errorRate: Math.round((parseFloat(row.error_rate ?? '0') * 100)),
|
||||
};
|
||||
}
|
||||
|
||||
async getRecentUsage(apiKeyId: ApiKeyId, limit: number): Promise<Array<{
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
}>> {
|
||||
const result = await this.pool.query(
|
||||
`SELECT timestamp, endpoint, method, status_code, response_time_ms
|
||||
FROM api_key_usage
|
||||
WHERE api_key_id = $1
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT $2`,
|
||||
[apiKeyId, limit]
|
||||
);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
timestamp: row.timestamp as Date,
|
||||
endpoint: row.endpoint as string,
|
||||
method: row.method as string,
|
||||
statusCode: row.status_code as number,
|
||||
responseTimeMs: row.response_time_ms as number,
|
||||
}));
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
@@ -36,3 +36,20 @@ export {
|
||||
PostgresArtifactRegistry,
|
||||
PostgresConfig,
|
||||
} from './adapters/postgresql';
|
||||
|
||||
// API Key repositories
|
||||
export {
|
||||
ApiKeyRepository,
|
||||
ApiKeyUsageRepository,
|
||||
} from './repositories/api-key-repository';
|
||||
|
||||
export {
|
||||
PostgresApiKeyRepository,
|
||||
PostgresApiKeyUsageRepository,
|
||||
PostgresApiKeyConfig,
|
||||
} from './adapters/postgresql/postgres-api-key-repository';
|
||||
|
||||
export {
|
||||
InMemoryApiKeyRepository,
|
||||
InMemoryApiKeyUsageRepository,
|
||||
} from './adapters/in-memory-api-key-repository';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* API Key Repository Interface
|
||||
*
|
||||
* Defines WHAT the domain needs from API key persistence.
|
||||
* Infrastructure adapters implement HOW.
|
||||
*/
|
||||
import { ApiKey, ApiKeyId } from '@landvex/domain';
|
||||
export interface ApiKeyRepository {
|
||||
save(apiKey: ApiKey): Promise<void>;
|
||||
findById(id: ApiKeyId): Promise<ApiKey | null>;
|
||||
findByKeyHash(keyHash: string): Promise<ApiKey | null>;
|
||||
findAll(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: string;
|
||||
}): Promise<ApiKey[]>;
|
||||
count(options?: {
|
||||
status?: string;
|
||||
}): Promise<number>;
|
||||
update(apiKey: ApiKey): Promise<void>;
|
||||
delete(id: ApiKeyId): Promise<void>;
|
||||
}
|
||||
export interface ApiKeyUsageRepository {
|
||||
recordUsage(usage: {
|
||||
apiKeyId: ApiKeyId;
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
clientIp?: string;
|
||||
}): Promise<void>;
|
||||
getUsageStats(apiKeyId: ApiKeyId, since: Date): Promise<{
|
||||
totalRequests: number;
|
||||
requestsPerMinute: number;
|
||||
requestsPerHour: number;
|
||||
requestsPerDay: number;
|
||||
averageResponseTimeMs: number;
|
||||
errorRate: number;
|
||||
}>;
|
||||
getRecentUsage(apiKeyId: ApiKeyId, limit: number): Promise<Array<{
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
}>>;
|
||||
}
|
||||
//# sourceMappingURL=api-key-repository.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"api-key-repository.d.ts","sourceRoot":"","sources":["api-key-repository.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvD,OAAO,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3F,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,KAAK,EAAE;QACjB,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,EAAE,IAAI,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC;QACtD,aAAa,EAAE,MAAM,CAAC;QACtB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IAEH,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAC/D,SAAS,EAAE,IAAI,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC,CAAC;CACL"}
|
||||
@@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
/**
|
||||
* API Key Repository Interface
|
||||
*
|
||||
* Defines WHAT the domain needs from API key persistence.
|
||||
* Infrastructure adapters implement HOW.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=api-key-repository.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"api-key-repository.js","sourceRoot":"","sources":["api-key-repository.ts"],"names":[],"mappings":";AAAA;;;;;GAKG"}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* API Key Repository Interface
|
||||
*
|
||||
* Defines WHAT the domain needs from API key persistence.
|
||||
* Infrastructure adapters implement HOW.
|
||||
*/
|
||||
|
||||
import { ApiKey, ApiKeyId } from '@landvex/domain';
|
||||
|
||||
export interface ApiKeyRepository {
|
||||
save(apiKey: ApiKey): Promise<void>;
|
||||
findById(id: ApiKeyId): Promise<ApiKey | null>;
|
||||
findByKeyHash(keyHash: string): Promise<ApiKey | null>;
|
||||
findAll(options?: { limit?: number; offset?: number; status?: string }): Promise<ApiKey[]>;
|
||||
count(options?: { status?: string }): Promise<number>;
|
||||
update(apiKey: ApiKey): Promise<void>;
|
||||
delete(id: ApiKeyId): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ApiKeyUsageRepository {
|
||||
recordUsage(usage: {
|
||||
apiKeyId: ApiKeyId;
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
clientIp?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
getUsageStats(apiKeyId: ApiKeyId, since: Date): Promise<{
|
||||
totalRequests: number;
|
||||
requestsPerMinute: number;
|
||||
requestsPerHour: number;
|
||||
requestsPerDay: number;
|
||||
averageResponseTimeMs: number;
|
||||
errorRate: number;
|
||||
}>;
|
||||
|
||||
getRecentUsage(apiKeyId: ApiKeyId, limit: number): Promise<Array<{
|
||||
timestamp: Date;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
}>>;
|
||||
}
|
||||
Reference in New Issue
Block a user