Upload files to "src/providers"
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { logUtil } from "../utils/logger";
|
||||
import type { ProviderName, ProviderResult } from "./types";
|
||||
/**
|
||||
* Minimal shape the streaming API needs from a Collector.
|
||||
* Keeps Provider decoupled from the Collector class.
|
||||
*/
|
||||
export interface ResultSink {
|
||||
ingest(result: ProviderResult): void;
|
||||
}
|
||||
|
||||
export abstract class Provider {
|
||||
provider: ProviderName;
|
||||
service: string;
|
||||
private patterns: Map<string, RegExp>;
|
||||
|
||||
abstract execute(): Promise<ProviderResult>;
|
||||
|
||||
constructor(
|
||||
provider: ProviderName,
|
||||
service: string,
|
||||
patterns?: Record<string, RegExp | string>,
|
||||
) {
|
||||
this.provider = provider;
|
||||
this.service = service;
|
||||
this.patterns = new Map();
|
||||
|
||||
if (patterns) {
|
||||
Object.entries(patterns).forEach(([key, pattern]) => {
|
||||
this.patterns.set(
|
||||
key,
|
||||
pattern instanceof RegExp ? pattern : new RegExp(pattern, "g"),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional streaming hook. Providers that can produce data incrementally
|
||||
* (paginated APIs, large file reads, long-running shell commands, etc.)
|
||||
* should override this to yield data chunks as they arrive.
|
||||
*
|
||||
* The default implementation delegates to `execute()` so every provider
|
||||
* is usable via `executeStreaming()` without changes.
|
||||
*/
|
||||
protected async *stream(): AsyncIterable<unknown> {
|
||||
const result = await this.execute();
|
||||
if (!result.success) {
|
||||
throw result.error ?? new Error("provider execute() failed");
|
||||
}
|
||||
if (result.data !== undefined) {
|
||||
yield result.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the provider and push each produced chunk to `sink` as soon as it
|
||||
* is available. Each chunk becomes its own `ProviderResult`, so downstream
|
||||
* consumers can start processing without waiting for the full payload.
|
||||
*
|
||||
* Errors are surfaced as a single failure result rather than thrown.
|
||||
*/
|
||||
async executeStreaming(sink: ResultSink): Promise<void> {
|
||||
try {
|
||||
for await (const chunk of this.stream()) {
|
||||
logUtil.info("Ingesting!");
|
||||
sink.ingest(this.success(chunk));
|
||||
}
|
||||
} catch (err) {
|
||||
sink.ingest(this.failure(err instanceof Error ? err : String(err)));
|
||||
}
|
||||
}
|
||||
|
||||
protected failure(error: Error | string): ProviderResult {
|
||||
return {
|
||||
provider: this.provider,
|
||||
service: this.service,
|
||||
success: false,
|
||||
error: error instanceof Error ? error : new Error(error),
|
||||
size: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private serializeData(data: unknown): string {
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data === null || data === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof data === "object") {
|
||||
try {
|
||||
return JSON.stringify(data, (_key, value) => {
|
||||
if (value instanceof Map) {
|
||||
return Object.fromEntries(value);
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
return Array.from(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
} catch {
|
||||
// Fallback for circular references or non-serializable objects
|
||||
if ("toString" in data && typeof data.toString === "function") {
|
||||
const str = data.toString();
|
||||
if (str !== "[object Object]") {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
/** Byte length of the serialized form, using UTF-8. */
|
||||
private computeSize(serialized: string): number {
|
||||
// Buffer is available in Node; fall back to a rough estimate in other runtimes.
|
||||
if (typeof Buffer !== "undefined") {
|
||||
return Buffer.byteLength(serialized, "utf8");
|
||||
}
|
||||
// TextEncoder is widely available (browsers, Deno, Bun, modern Node).
|
||||
if (typeof TextEncoder !== "undefined") {
|
||||
return new TextEncoder().encode(serialized).length;
|
||||
}
|
||||
return serialized.length;
|
||||
}
|
||||
|
||||
protected success(data: unknown): ProviderResult {
|
||||
const dataStr = this.serializeData(data);
|
||||
|
||||
const result: ProviderResult = {
|
||||
provider: this.provider,
|
||||
service: this.service,
|
||||
success: true,
|
||||
data,
|
||||
size: this.computeSize(dataStr),
|
||||
};
|
||||
|
||||
if (this.patterns.size > 0) {
|
||||
const matches: Record<string, string[]> = {};
|
||||
|
||||
this.patterns.forEach((regex, key) => {
|
||||
const found = Array.from(dataStr.matchAll(regex)).map((m) => m[0]);
|
||||
const deduplicated = Array.from(new Set(found));
|
||||
|
||||
if (deduplicated.length > 0) {
|
||||
matches[key] = deduplicated;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(matches).length > 0) {
|
||||
result.matches = matches;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// This class represents a "provider" essentially a source for values.
|
||||
//
|
||||
export type ProviderName =
|
||||
| "aws"
|
||||
| "azure"
|
||||
| "gcp"
|
||||
| "filesystem"
|
||||
| "github"
|
||||
| "shell"
|
||||
| "vault"
|
||||
| "kubernetes";
|
||||
|
||||
export interface ProviderResult {
|
||||
provider: ProviderName;
|
||||
service: string;
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
matches?: Record<string, string[]>;
|
||||
error?: Error | undefined;
|
||||
size: number;
|
||||
}
|
||||
Reference in New Issue
Block a user