feat(portal-bff): ai-client skeleton — vendored protos + grpc client + Principal mapper #195
@@ -5,3 +5,8 @@
|
||||
/.nx/workspace-data
|
||||
.nx/self-healing
|
||||
.angular
|
||||
|
||||
# Generated gRPC TypeScript stubs (output of `pnpm run grpc:codegen`).
|
||||
# Hand-formatting is not productive — overwritten on next regen — and
|
||||
# ts-proto's output is internally consistent.
|
||||
apps/portal-bff/src/grpc/gen/
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, beforeEach, afterAll } from '@jest/globals';
|
||||
import { assertAiServiceConfig } from './check-ai-service-config';
|
||||
|
||||
/**
|
||||
* Pre-flight validation contract for `AI_SERVICE_*` env vars.
|
||||
* Mirrors the test posture of `check-log-user-id-salt.spec.ts` —
|
||||
* each rejection path produces an actionable error message.
|
||||
*/
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
function setEnv(values: Record<string, string | undefined>): void {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env['AI_SERVICE_GRPC_ENDPOINT'];
|
||||
delete process.env['AI_SERVICE_CLIENT_ID'];
|
||||
delete process.env['AI_SERVICE_GRPC_TLS'];
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
});
|
||||
|
||||
describe('assertAiServiceConfig', () => {
|
||||
it('returns the parsed config when every required var is present', () => {
|
||||
setEnv({
|
||||
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080',
|
||||
AI_SERVICE_CLIENT_ID: 'apf-portal-dev',
|
||||
AI_SERVICE_GRPC_TLS: 'false',
|
||||
});
|
||||
|
||||
expect(assertAiServiceConfig()).toEqual({
|
||||
endpoint: 'apf-ai-service:8080',
|
||||
clientId: 'apf-portal-dev',
|
||||
useTls: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults useTls to true when AI_SERVICE_GRPC_TLS is unset', () => {
|
||||
setEnv({
|
||||
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai.internal:443',
|
||||
AI_SERVICE_CLIENT_ID: 'apf-portal-prod',
|
||||
});
|
||||
|
||||
expect(assertAiServiceConfig().useTls).toBe(true);
|
||||
});
|
||||
|
||||
it('throws when AI_SERVICE_GRPC_ENDPOINT is missing', () => {
|
||||
setEnv({ AI_SERVICE_CLIENT_ID: 'apf-portal-dev' });
|
||||
expect(() => assertAiServiceConfig()).toThrow(/AI_SERVICE_GRPC_ENDPOINT is not set/);
|
||||
});
|
||||
|
||||
it('throws when AI_SERVICE_GRPC_ENDPOINT does not match host:port', () => {
|
||||
setEnv({
|
||||
AI_SERVICE_GRPC_ENDPOINT: 'no-port-here',
|
||||
AI_SERVICE_CLIENT_ID: 'apf-portal-dev',
|
||||
});
|
||||
expect(() => assertAiServiceConfig()).toThrow(/must match "host:port"/);
|
||||
});
|
||||
|
||||
it('throws when AI_SERVICE_CLIENT_ID is missing', () => {
|
||||
setEnv({ AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080' });
|
||||
expect(() => assertAiServiceConfig()).toThrow(/AI_SERVICE_CLIENT_ID is not set/);
|
||||
});
|
||||
|
||||
it('throws when AI_SERVICE_CLIENT_ID has invalid characters', () => {
|
||||
setEnv({
|
||||
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080',
|
||||
AI_SERVICE_CLIENT_ID: 'Apf Portal Dev',
|
||||
});
|
||||
expect(() => assertAiServiceConfig()).toThrow(/lowercase kebab-case slug/);
|
||||
});
|
||||
|
||||
it('throws when AI_SERVICE_GRPC_TLS is neither "true" nor "false"', () => {
|
||||
setEnv({
|
||||
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080',
|
||||
AI_SERVICE_CLIENT_ID: 'apf-portal-dev',
|
||||
AI_SERVICE_GRPC_TLS: 'yes',
|
||||
});
|
||||
expect(() => assertAiServiceConfig()).toThrow(/must be "true" or "false"/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Validate the env vars that drive the BFF's gRPC client to
|
||||
* `apf-ai-service`, per ADR-0024.
|
||||
*
|
||||
* Three knobs, all required at module-init time of `AiClientModule`:
|
||||
*
|
||||
* - `AI_SERVICE_GRPC_ENDPOINT` — `host:port` reachable from the
|
||||
* BFF. `apf-ai-service:8080` in the dev Compose network;
|
||||
* a service DNS name + 443 in prod behind an edge proxy that
|
||||
* terminates the public TLS and forwards h2 internally.
|
||||
* - `AI_SERVICE_CLIENT_ID` — deployment slug propagated as the
|
||||
* `x-client-id` gRPC metadata on every call (per
|
||||
* `apf-ai-service/docs/contract.md`). Naming convention:
|
||||
* `apf-portal-<env>` (`apf-portal-dev`, `apf-portal-preprod`,
|
||||
* `apf-portal-prod`).
|
||||
* - `AI_SERVICE_GRPC_TLS` — `'true'` to dial with TLS credentials
|
||||
* against a CA-signed cert (default for non-dev), `'false'` for
|
||||
* h2c against the Compose network in dev. Any other value is
|
||||
* rejected to keep the cleartext-vs-TLS toggle explicit.
|
||||
*
|
||||
* Following the same family as `assertLogUserIdSalt()` and the
|
||||
* other config validators under `apps/portal-bff/src/config/`,
|
||||
* per ADR-0018 §"BFF env-var loading" — small, per-key, run at
|
||||
* bootstrap, throw with an actionable message on misconfiguration.
|
||||
*/
|
||||
|
||||
export interface AiServiceConfig {
|
||||
readonly endpoint: string;
|
||||
readonly clientId: string;
|
||||
readonly useTls: boolean;
|
||||
}
|
||||
|
||||
const ENDPOINT_PATTERN = /^[a-z0-9.-]+:[0-9]{2,5}$/i;
|
||||
const CLIENT_ID_PATTERN = /^[a-z0-9-]+$/;
|
||||
|
||||
export function assertAiServiceConfig(): AiServiceConfig {
|
||||
const endpoint = process.env['AI_SERVICE_GRPC_ENDPOINT'];
|
||||
if (!endpoint || endpoint === '') {
|
||||
throw new Error(
|
||||
`AI_SERVICE_GRPC_ENDPOINT is not set. Expected "host:port" reachable from the BFF, ` +
|
||||
`e.g. "apf-ai-service:8080" in dev Compose or "apf-ai.internal:443" in prod.`,
|
||||
);
|
||||
}
|
||||
if (!ENDPOINT_PATTERN.test(endpoint)) {
|
||||
throw new Error(
|
||||
`AI_SERVICE_GRPC_ENDPOINT must match "host:port" (got: ${JSON.stringify(endpoint)}). ` +
|
||||
`Hostname uses a-z 0-9 . - characters; port is 2-5 digits.`,
|
||||
);
|
||||
}
|
||||
|
||||
const clientId = process.env['AI_SERVICE_CLIENT_ID'];
|
||||
if (!clientId || clientId === '') {
|
||||
throw new Error(
|
||||
`AI_SERVICE_CLIENT_ID is not set. Use a deployment slug like ` +
|
||||
`"apf-portal-dev" / "apf-portal-preprod" / "apf-portal-prod"; ` +
|
||||
`propagated as the x-client-id gRPC metadata on every call.`,
|
||||
);
|
||||
}
|
||||
if (!CLIENT_ID_PATTERN.test(clientId)) {
|
||||
throw new Error(
|
||||
`AI_SERVICE_CLIENT_ID must be a lowercase kebab-case slug ` +
|
||||
`(got: ${JSON.stringify(clientId)}). Pattern: ^[a-z0-9-]+$.`,
|
||||
);
|
||||
}
|
||||
|
||||
const tlsRaw = process.env['AI_SERVICE_GRPC_TLS'] ?? 'true';
|
||||
if (tlsRaw !== 'true' && tlsRaw !== 'false') {
|
||||
throw new Error(
|
||||
`AI_SERVICE_GRPC_TLS must be "true" or "false" (got: ${JSON.stringify(tlsRaw)}). ` +
|
||||
`Default in prod is "true"; the dev Compose stack sets it to "false" for h2c.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
endpoint,
|
||||
clientId,
|
||||
useTls: tlsRaw === 'true',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { AiClientModule } from './ai-client.module';
|
||||
import { ChatClient } from './chat.client';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { IngestionClient } from './ingestion.client';
|
||||
import { ModelsClient } from './models.client';
|
||||
import { PrincipalMapper } from './principal.mapper';
|
||||
import { RagClient } from './rag.client';
|
||||
import {
|
||||
AI_CHAT_GRPC_CLIENT,
|
||||
AI_CONFIG,
|
||||
AI_CREDENTIALS,
|
||||
AI_INGESTION_GRPC_CLIENT,
|
||||
AI_MODELS_GRPC_CLIENT,
|
||||
AI_RAG_GRPC_CLIENT,
|
||||
} from './tokens';
|
||||
|
||||
/**
|
||||
* Smoke-tests `AiClientModule.compile()` — every provider resolves,
|
||||
* the four exported wrapper clients are constructable, and the gRPC
|
||||
* stubs share the same credentials instance (i.e. the underlying
|
||||
* HTTP/2 channel can be multiplexed by grpc-js).
|
||||
*
|
||||
* Does NOT exercise the wire — the wire is covered by the per-client
|
||||
* fake-server specs.
|
||||
*/
|
||||
|
||||
const STRONG_SALT = randomBytes(32).toString('base64url');
|
||||
const ORIGINAL = {
|
||||
salt: process.env['LOG_USER_ID_SALT'],
|
||||
endpoint: process.env['AI_SERVICE_GRPC_ENDPOINT'],
|
||||
clientId: process.env['AI_SERVICE_CLIENT_ID'],
|
||||
tls: process.env['AI_SERVICE_GRPC_TLS'],
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
process.env['LOG_USER_ID_SALT'] = STRONG_SALT;
|
||||
process.env['AI_SERVICE_GRPC_ENDPOINT'] = 'apf-ai-service:8080';
|
||||
process.env['AI_SERVICE_CLIENT_ID'] = 'apf-portal-test';
|
||||
process.env['AI_SERVICE_GRPC_TLS'] = 'false';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (ORIGINAL.salt === undefined) delete process.env['LOG_USER_ID_SALT'];
|
||||
else process.env['LOG_USER_ID_SALT'] = ORIGINAL.salt;
|
||||
if (ORIGINAL.endpoint === undefined) delete process.env['AI_SERVICE_GRPC_ENDPOINT'];
|
||||
else process.env['AI_SERVICE_GRPC_ENDPOINT'] = ORIGINAL.endpoint;
|
||||
if (ORIGINAL.clientId === undefined) delete process.env['AI_SERVICE_CLIENT_ID'];
|
||||
else process.env['AI_SERVICE_CLIENT_ID'] = ORIGINAL.clientId;
|
||||
if (ORIGINAL.tls === undefined) delete process.env['AI_SERVICE_GRPC_TLS'];
|
||||
else process.env['AI_SERVICE_GRPC_TLS'] = ORIGINAL.tls;
|
||||
});
|
||||
|
||||
describe('AiClientModule', () => {
|
||||
let moduleRef: Awaited<ReturnType<ReturnType<typeof Test.createTestingModule>['compile']>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
// AiClientModule provides HashUserIdService locally — see the
|
||||
// module docstring for the rationale — so the test does not
|
||||
// need to bring in AuditModule (and AuditWriter's Prisma + CLS
|
||||
// dependencies that would imply).
|
||||
moduleRef = await Test.createTestingModule({
|
||||
imports: [AiClientModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('resolves all four wrapper clients', () => {
|
||||
expect(moduleRef.get(ChatClient)).toBeInstanceOf(ChatClient);
|
||||
expect(moduleRef.get(RagClient)).toBeInstanceOf(RagClient);
|
||||
expect(moduleRef.get(IngestionClient)).toBeInstanceOf(IngestionClient);
|
||||
expect(moduleRef.get(ModelsClient)).toBeInstanceOf(ModelsClient);
|
||||
});
|
||||
|
||||
it('resolves the Principal mapper and the metadata builder', () => {
|
||||
expect(moduleRef.get(PrincipalMapper)).toBeInstanceOf(PrincipalMapper);
|
||||
expect(moduleRef.get(GrpcMetadataBuilder)).toBeInstanceOf(GrpcMetadataBuilder);
|
||||
});
|
||||
|
||||
it('parses the env vars into AI_CONFIG', () => {
|
||||
const config = moduleRef.get(AI_CONFIG) as {
|
||||
endpoint: string;
|
||||
clientId: string;
|
||||
useTls: boolean;
|
||||
};
|
||||
expect(config).toEqual({
|
||||
endpoint: 'apf-ai-service:8080',
|
||||
clientId: 'apf-portal-test',
|
||||
useTls: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('shares one credentials instance across every generated stub', () => {
|
||||
const credentials = moduleRef.get(AI_CREDENTIALS);
|
||||
expect(credentials).toBeDefined();
|
||||
// Each stub provider injects AI_CREDENTIALS — same reference
|
||||
// means the underlying HTTP/2 connection can be multiplexed by
|
||||
// grpc-js when they target the same address.
|
||||
const chat = moduleRef.get(AI_CHAT_GRPC_CLIENT) as unknown;
|
||||
const rag = moduleRef.get(AI_RAG_GRPC_CLIENT) as unknown;
|
||||
const ingestion = moduleRef.get(AI_INGESTION_GRPC_CLIENT) as unknown;
|
||||
const models = moduleRef.get(AI_MODELS_GRPC_CLIENT) as unknown;
|
||||
expect(chat).toBeDefined();
|
||||
expect(rag).toBeDefined();
|
||||
expect(ingestion).toBeDefined();
|
||||
expect(models).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Module, type Provider } from '@nestjs/common';
|
||||
import { ChannelCredentials } from '@grpc/grpc-js';
|
||||
import { HashUserIdService } from '../../audit/hash-user-id.service';
|
||||
import { assertAiServiceConfig, type AiServiceConfig } from '../../config/check-ai-service-config';
|
||||
import { ChatServiceClient } from '../gen/apf-ai/chat';
|
||||
import { IngestionServiceClient } from '../gen/apf-ai/ingestion';
|
||||
import { ModelsServiceClient } from '../gen/apf-ai/models';
|
||||
import { RagServiceClient } from '../gen/apf-ai/rag';
|
||||
import { ChatClient } from './chat.client';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { IngestionClient } from './ingestion.client';
|
||||
import { ModelsClient } from './models.client';
|
||||
import { PrincipalMapper } from './principal.mapper';
|
||||
import { RagClient } from './rag.client';
|
||||
import {
|
||||
AI_CHAT_GRPC_CLIENT,
|
||||
AI_CONFIG,
|
||||
AI_CREDENTIALS,
|
||||
AI_INGESTION_GRPC_CLIENT,
|
||||
AI_MODELS_GRPC_CLIENT,
|
||||
AI_RAG_GRPC_CLIENT,
|
||||
} from './tokens';
|
||||
|
||||
/**
|
||||
* Wires the BFF's integration surface to `apf-ai-service`, per
|
||||
* [ADR-0024](../../../../../docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md).
|
||||
*
|
||||
* The module is intentionally NOT imported in `AppModule` yet — the
|
||||
* SSE-bridge controller that consumes these clients ships in the
|
||||
* next chantier. Until then the module compiles, type-checks, and
|
||||
* unit-tests against an in-process fake gRPC server, but it does not
|
||||
* register any HTTP route.
|
||||
*
|
||||
* Provider layout:
|
||||
*
|
||||
* 1. `AI_CONFIG` factory reads + validates `AI_SERVICE_*` env vars.
|
||||
* 2. `AI_CREDENTIALS` factory builds the gRPC channel credentials
|
||||
* (insecure h2c in dev, system-CA-trusted SSL in prod).
|
||||
* 3. One token per generated stub — `AI_CHAT_GRPC_CLIENT` etc. —
|
||||
* backed by a factory that opens a `Client` against the
|
||||
* configured endpoint. gRPC-js multiplexes the underlying
|
||||
* HTTP/2 connection across stubs sharing the same address +
|
||||
* credentials, so the four stubs share one TCP+TLS channel.
|
||||
* 4. Hand-written wrapper services (`ChatClient`, `RagClient`,
|
||||
* `IngestionClient`, `ModelsClient`) add metadata injection
|
||||
* and promisification on top of the generated stubs.
|
||||
* 5. `PrincipalMapper` + `GrpcMetadataBuilder` collaborators.
|
||||
*
|
||||
* `PrincipalMapper` depends on `HashUserIdService` — the same hash
|
||||
* service the audit writer uses — so the `Principal.subject` field
|
||||
* matches `audit.events.actor_id_hash` exactly (per ADR-0024
|
||||
* §"Audit cross-referencing"). `HashUserIdService` is declared as a
|
||||
* local provider here (rather than relying on `AuditModule`'s
|
||||
* `@Global()` export) for two reasons:
|
||||
*
|
||||
* 1. **Test isolation.** The module compiles standalone with no
|
||||
* other portal module in scope; no need to pull `AuditWriter`
|
||||
* and its Prisma + CLS deps into a unit test that only exercises
|
||||
* the gRPC surface.
|
||||
* 2. **Self-containment of the wire boundary.** The Principal-side
|
||||
* contract is a property of `AiClientModule`, not of the audit
|
||||
* module. Co-locating the hash provider keeps the boundary
|
||||
* readable from one file.
|
||||
*
|
||||
* Functional identity with the audit-side instance is guaranteed by
|
||||
* `HashUserIdService` being purely a function of
|
||||
* `LOG_USER_ID_SALT` — same env value ⇒ same hash output, regardless
|
||||
* of how many instances exist. The cross-service join key invariant
|
||||
* from ADR-0013 still holds.
|
||||
*/
|
||||
|
||||
function buildCredentials(config: AiServiceConfig): ChannelCredentials {
|
||||
return config.useTls ? ChannelCredentials.createSsl() : ChannelCredentials.createInsecure();
|
||||
}
|
||||
|
||||
const grpcStubProviders: Provider[] = [
|
||||
{
|
||||
provide: AI_CHAT_GRPC_CLIENT,
|
||||
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||
new ChatServiceClient(config.endpoint, credentials),
|
||||
},
|
||||
{
|
||||
provide: AI_RAG_GRPC_CLIENT,
|
||||
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||
new RagServiceClient(config.endpoint, credentials),
|
||||
},
|
||||
{
|
||||
provide: AI_INGESTION_GRPC_CLIENT,
|
||||
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||
new IngestionServiceClient(config.endpoint, credentials),
|
||||
},
|
||||
{
|
||||
provide: AI_MODELS_GRPC_CLIENT,
|
||||
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||
new ModelsServiceClient(config.endpoint, credentials),
|
||||
},
|
||||
];
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: AI_CONFIG,
|
||||
useFactory: () => assertAiServiceConfig(),
|
||||
},
|
||||
{
|
||||
provide: AI_CREDENTIALS,
|
||||
inject: [AI_CONFIG],
|
||||
useFactory: buildCredentials,
|
||||
},
|
||||
...grpcStubProviders,
|
||||
HashUserIdService,
|
||||
GrpcMetadataBuilder,
|
||||
PrincipalMapper,
|
||||
ChatClient,
|
||||
RagClient,
|
||||
IngestionClient,
|
||||
ModelsClient,
|
||||
],
|
||||
exports: [ChatClient, RagClient, IngestionClient, ModelsClient, PrincipalMapper],
|
||||
})
|
||||
export class AiClientModule {
|
||||
// Lifecycle (channel close on shutdown) lands when the SSE-bridge
|
||||
// PR wires this module into AppModule. v1 process termination
|
||||
// closes the gRPC sockets via OS-level descriptor reclaim — fine
|
||||
// for the dev/preprod posture; prod will add an explicit
|
||||
// OnApplicationShutdown hook once Nest's shutdown hooks are
|
||||
// enabled in main.ts.
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||
import {
|
||||
ChannelCredentials,
|
||||
Server,
|
||||
ServerCredentials,
|
||||
type Metadata,
|
||||
type ServerWritableStream,
|
||||
} from '@grpc/grpc-js';
|
||||
import {
|
||||
ChatServiceClient,
|
||||
ChatServiceService,
|
||||
type ChatEvent,
|
||||
type ChatRequest,
|
||||
} from '../gen/apf-ai/chat';
|
||||
import { ChatClient } from './chat.client';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||
|
||||
/**
|
||||
* Integration test: drives `ChatClient` against an in-process fake
|
||||
* gRPC ChatService that emits a canned `ChatEvent` sequence. The
|
||||
* spec covers ADR-0024's confirmation criteria:
|
||||
*
|
||||
* - happy-path stream emits every event the server sent and ends
|
||||
* after `done`;
|
||||
* - metadata (`x-client-id` + `x-correlation-id`) reaches the
|
||||
* server unchanged;
|
||||
* - browser-close (modelled here as an `AbortController.abort()`)
|
||||
* propagates as `call.cancel()` and the server's stream callback
|
||||
* observes the cancellation.
|
||||
*/
|
||||
|
||||
const CONFIG: AiServiceConfig = {
|
||||
endpoint: '',
|
||||
clientId: 'apf-portal-test',
|
||||
useTls: false,
|
||||
};
|
||||
|
||||
interface ServerObservations {
|
||||
request: ChatRequest | null;
|
||||
metadata: Metadata | null;
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
let server: Server;
|
||||
let port: number;
|
||||
let serverObservations: ServerObservations;
|
||||
let chatHandler: (call: ServerWritableStream<ChatRequest, ChatEvent>) => void;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = new Server();
|
||||
server.addService(ChatServiceService, {
|
||||
chat: (call: ServerWritableStream<ChatRequest, ChatEvent>) => {
|
||||
serverObservations.request = call.request;
|
||||
serverObservations.metadata = call.metadata;
|
||||
call.on('cancelled', () => {
|
||||
serverObservations.cancelled = true;
|
||||
});
|
||||
chatHandler(call);
|
||||
},
|
||||
});
|
||||
|
||||
port = await new Promise<number>((resolve, reject) => {
|
||||
server.bindAsync('127.0.0.1:0', ServerCredentials.createInsecure(), (err, bound) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(bound);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.tryShutdown((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
serverObservations = { request: null, metadata: null, cancelled: false };
|
||||
});
|
||||
|
||||
function buildClient(): ChatClient {
|
||||
const config = { ...CONFIG, endpoint: `127.0.0.1:${port}` };
|
||||
const grpc = new ChatServiceClient(config.endpoint, ChannelCredentials.createInsecure());
|
||||
return new ChatClient(grpc, new GrpcMetadataBuilder(config));
|
||||
}
|
||||
|
||||
async function collect(stream: AsyncIterable<ChatEvent>): Promise<ChatEvent[]> {
|
||||
const events: ChatEvent[] = [];
|
||||
for await (const event of stream) {
|
||||
events.push(event);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
describe('ChatClient (in-process fake server)', () => {
|
||||
it('emits every server event then terminates on done', async () => {
|
||||
chatHandler = (call) => {
|
||||
call.write({ token: { token: 'hello', value: 'Hello' } });
|
||||
call.write({ token: { token: ' world', value: ' world' } });
|
||||
call.write({ done: { stats: { tokensIn: 1, tokensOut: 2, chunksRetrieved: 0 } } });
|
||||
call.end();
|
||||
};
|
||||
|
||||
const client = buildClient();
|
||||
const stream = client.chat({
|
||||
messages: [],
|
||||
conversationId: 'conv-1',
|
||||
model: '',
|
||||
provider: '',
|
||||
toolsAvailable: [],
|
||||
principal: { subject: 'subj-1', roles: ['admin'], attributes: { tenantId: 't' } },
|
||||
});
|
||||
const events = await collect(stream);
|
||||
|
||||
expect(events).toHaveLength(3);
|
||||
expect(events[0]?.token?.value).toBe('Hello');
|
||||
expect(events[2]?.done?.stats?.tokensOut).toBe(2);
|
||||
});
|
||||
|
||||
it('propagates x-client-id and x-correlation-id metadata to the server', async () => {
|
||||
chatHandler = (call) => {
|
||||
call.write({ done: { stats: { tokensIn: 0, tokensOut: 0, chunksRetrieved: 0 } } });
|
||||
call.end();
|
||||
};
|
||||
|
||||
const client = buildClient();
|
||||
await collect(
|
||||
client.chat(
|
||||
{
|
||||
messages: [],
|
||||
conversationId: 'conv-2',
|
||||
model: '',
|
||||
provider: '',
|
||||
toolsAvailable: [],
|
||||
principal: { subject: 'subj-2', roles: [], attributes: {} },
|
||||
},
|
||||
{ correlationId: 'corr-from-test' },
|
||||
),
|
||||
);
|
||||
|
||||
expect(serverObservations.metadata?.get('x-client-id')).toEqual(['apf-portal-test']);
|
||||
expect(serverObservations.metadata?.get('x-correlation-id')).toEqual(['corr-from-test']);
|
||||
});
|
||||
|
||||
it('cancels the call when the AbortSignal aborts mid-stream', async () => {
|
||||
const cancellationObserved = new Promise<void>((resolve) => {
|
||||
chatHandler = (call) => {
|
||||
call.write({ token: { token: 't1', value: 't1' } });
|
||||
call.on('cancelled', () => resolve());
|
||||
// Deliberately do not `end()` — the test cancels.
|
||||
};
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
const client = buildClient();
|
||||
const stream = client.chat(
|
||||
{
|
||||
messages: [],
|
||||
conversationId: 'conv-3',
|
||||
model: '',
|
||||
provider: '',
|
||||
toolsAvailable: [],
|
||||
principal: { subject: 'subj-3', roles: [], attributes: {} },
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
// Drain the first event then abort.
|
||||
const iter = stream[Symbol.asyncIterator]();
|
||||
await iter.next();
|
||||
controller.abort();
|
||||
|
||||
await cancellationObserved;
|
||||
expect(serverObservations.cancelled).toBe(true);
|
||||
});
|
||||
|
||||
it('terminates the stream without data when the AbortSignal is already aborted', async () => {
|
||||
// The signal aborts before the call dial completes. gRPC-js
|
||||
// cancels locally — depending on timing the server may or may
|
||||
// not observe the call, so the spec only locks the client-side
|
||||
// outcome: the stream ends in error and yields no payload.
|
||||
chatHandler = (call) => {
|
||||
call.on('cancelled', () => {
|
||||
// best-effort end on cancellation so the suite never hangs
|
||||
try {
|
||||
call.end();
|
||||
} catch {
|
||||
// already torn down
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
const client = buildClient();
|
||||
const stream = client.chat(
|
||||
{
|
||||
messages: [],
|
||||
conversationId: 'conv-4',
|
||||
model: '',
|
||||
provider: '',
|
||||
toolsAvailable: [],
|
||||
principal: { subject: 'subj-4', roles: [], attributes: {} },
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
const events: ChatEvent[] = [];
|
||||
let caught: unknown;
|
||||
try {
|
||||
for await (const ev of stream) {
|
||||
events.push(ev);
|
||||
}
|
||||
} catch (err) {
|
||||
caught = err;
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(0);
|
||||
// gRPC-js may surface the cancellation as an error or as a clean
|
||||
// end-of-stream depending on whether the call had time to dial.
|
||||
// Either way, no payload is emitted — that is the contract.
|
||||
if (caught !== undefined) {
|
||||
expect((caught as { code?: number }).code).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { ClientReadableStream } from '@grpc/grpc-js';
|
||||
import type { ChatEvent, ChatRequest, ChatServiceClient } from '../gen/apf-ai/chat';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { AI_CHAT_GRPC_CLIENT } from './tokens';
|
||||
|
||||
/**
|
||||
* Wrapper around the generated `ChatServiceClient` for the
|
||||
* server-streaming `Chat` RPC, per ADR-0024 §"Sub-decision 1".
|
||||
*
|
||||
* The wrapper does three things on top of the raw gRPC stub:
|
||||
*
|
||||
* 1. Injects the `x-client-id` + `x-correlation-id` metadata via
|
||||
* `GrpcMetadataBuilder` so every call carries the contract
|
||||
* headers from `apf-ai-service/docs/contract.md`.
|
||||
* 2. Wires an optional `AbortSignal` to `call.cancel()` so the SSE
|
||||
* bridge (next PR in the chantier) can propagate browser
|
||||
* disconnects up to the AI service in one line.
|
||||
* 3. Returns the raw `ClientReadableStream<ChatEvent>` — Node
|
||||
* `Readable` streams are async-iterable by default, so the SSE
|
||||
* bridge consumes the stream with `for await ... of` and no
|
||||
* intermediate adapter.
|
||||
*
|
||||
* Cancellation flows in both directions: a `.cancel()` from inside
|
||||
* the BFF (timeout, abort) reaches the AI service as a
|
||||
* `grpc.status.CANCELLED`, which translates upstream to
|
||||
* `ServerCallContext.CancellationToken` stopping LLM generation
|
||||
* (per `apf-ai-service/docs/streaming.md`).
|
||||
*/
|
||||
@Injectable()
|
||||
export class ChatClient {
|
||||
constructor(
|
||||
@Inject(AI_CHAT_GRPC_CLIENT) private readonly grpc: ChatServiceClient,
|
||||
private readonly metadata: GrpcMetadataBuilder,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Start a streaming Chat call.
|
||||
*
|
||||
* The returned `ClientReadableStream<ChatEvent>` emits one event
|
||||
* per AI service `ChatEvent` and ends after the terminal
|
||||
* `ChatEvent.done` (or on cancellation / error).
|
||||
*
|
||||
* @param request The full proto request, already populated with
|
||||
* the conversation history and a `Principal` (build the
|
||||
* principal via `PrincipalMapper.fromInputs`).
|
||||
* @param options.signal Optional AbortSignal. When it aborts, the
|
||||
* underlying gRPC call is cancelled and the stream closes.
|
||||
* @param options.correlationId Override the metadata
|
||||
* correlation-id (defaults to the active OTel trace-id).
|
||||
*/
|
||||
chat(
|
||||
request: ChatRequest,
|
||||
options: { signal?: AbortSignal; correlationId?: string } = {},
|
||||
): ClientReadableStream<ChatEvent> {
|
||||
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||
const call = this.grpc.chat(request, metadata);
|
||||
|
||||
if (options.signal) {
|
||||
if (options.signal.aborted) {
|
||||
call.cancel();
|
||||
} else {
|
||||
options.signal.addEventListener('abort', () => call.cancel(), { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
return call;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||
|
||||
/**
|
||||
* Locks the metadata-injection contract per ADR-0024 §"Metadata contract":
|
||||
*
|
||||
* - x-client-id ← AI_SERVICE_CLIENT_ID env (via AI_CONFIG).
|
||||
* - x-correlation-id ← active OTel span's trace-id when present;
|
||||
* explicit `correlationId` override wins over
|
||||
* the span; otherwise a fresh UUID is minted
|
||||
* so the AI service can still join its audit
|
||||
* row to a unique id even when the portal
|
||||
* request is outside a trace.
|
||||
*/
|
||||
|
||||
const CONFIG: AiServiceConfig = {
|
||||
endpoint: 'apf-ai-service:8080',
|
||||
clientId: 'apf-portal-test',
|
||||
useTls: false,
|
||||
};
|
||||
|
||||
let activeSpan: ReturnType<typeof trace.getActiveSpan> = undefined;
|
||||
let getActiveSpanSpy: ReturnType<typeof jest.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeSpan = undefined;
|
||||
getActiveSpanSpy = jest.spyOn(trace, 'getActiveSpan').mockImplementation(() => activeSpan);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getActiveSpanSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('GrpcMetadataBuilder', () => {
|
||||
it('always emits x-client-id from the config', () => {
|
||||
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||
const meta = builder.build();
|
||||
expect(meta.get('x-client-id')).toEqual(['apf-portal-test']);
|
||||
});
|
||||
|
||||
it('uses the active OTel trace-id as x-correlation-id when a span is active', () => {
|
||||
activeSpan = {
|
||||
spanContext: () => ({
|
||||
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
spanId: 'bbbbbbbbbbbbbbbb',
|
||||
traceFlags: 1,
|
||||
}),
|
||||
} as ReturnType<typeof trace.getActiveSpan>;
|
||||
|
||||
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||
const meta = builder.build();
|
||||
expect(meta.get('x-correlation-id')).toEqual(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']);
|
||||
});
|
||||
|
||||
it('prefers the explicit options.correlationId over the active span', () => {
|
||||
activeSpan = {
|
||||
spanContext: () => ({
|
||||
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
spanId: 'bbbbbbbbbbbbbbbb',
|
||||
traceFlags: 1,
|
||||
}),
|
||||
} as ReturnType<typeof trace.getActiveSpan>;
|
||||
|
||||
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||
const meta = builder.build({ correlationId: 'caller-supplied-id' });
|
||||
expect(meta.get('x-correlation-id')).toEqual(['caller-supplied-id']);
|
||||
});
|
||||
|
||||
it('falls back to a UUID v4 when no span is active and no override is passed', () => {
|
||||
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||
const meta = builder.build();
|
||||
const correlationIds = meta.get('x-correlation-id') as string[];
|
||||
expect(correlationIds).toHaveLength(1);
|
||||
expect(correlationIds[0]).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a fresh Metadata instance on every call (no shared mutation)', () => {
|
||||
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||
const a = builder.build();
|
||||
const b = builder.build();
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { trace } from '@opentelemetry/api';
|
||||
import { AI_CONFIG } from './tokens';
|
||||
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||
|
||||
/**
|
||||
* Builds the gRPC `Metadata` object every outbound call to
|
||||
* `apf-ai-service` must carry, per ADR-0024 §"Metadata contract".
|
||||
*
|
||||
* Two entries:
|
||||
*
|
||||
* - `x-client-id` — deployment slug from `AI_SERVICE_CLIENT_ID`.
|
||||
* Lets the AI service tag its audit rows and metrics with the
|
||||
* calling deployment without inspecting the proto body.
|
||||
* - `x-correlation-id` — W3C `trace-id` of the active OpenTelemetry
|
||||
* span when one exists (per [ADR-0012](../../docs/decisions/0012-observability-pino-opentelemetry.md)),
|
||||
* falling back to an explicit value if passed, falling back to a
|
||||
* freshly-minted UUID v4 as a last resort. The AI service echoes
|
||||
* this id in its audit log and propagates it to Jaeger, so the
|
||||
* portal and AI traces can be joined cross-service.
|
||||
*
|
||||
* The builder is a class (not a static helper) so it composes with
|
||||
* NestJS DI and so it can be swapped for a fake in tests that need
|
||||
* to assert the metadata shape without standing up the OTel SDK.
|
||||
*/
|
||||
@Injectable()
|
||||
export class GrpcMetadataBuilder {
|
||||
constructor(@Inject(AI_CONFIG) private readonly config: AiServiceConfig) {}
|
||||
|
||||
/**
|
||||
* Construct the metadata for one outbound call.
|
||||
*
|
||||
* @param options.correlationId Override the auto-resolved value.
|
||||
* Use when the caller already holds a stable id (for example
|
||||
* the SSE bridge controller's request-scoped trace-id) and
|
||||
* wants to bind multiple calls to the same correlation key.
|
||||
* `undefined` is accepted (and equivalent to omitting the
|
||||
* key) so callers can forward an optional value without
|
||||
* a conditional spread.
|
||||
*/
|
||||
build(options: { correlationId?: string | undefined } = {}): Metadata {
|
||||
const meta = new Metadata();
|
||||
meta.set('x-client-id', this.config.clientId);
|
||||
|
||||
const correlationId =
|
||||
options.correlationId ?? trace.getActiveSpan()?.spanContext().traceId ?? randomUUID();
|
||||
meta.set('x-correlation-id', correlationId);
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type {
|
||||
DeleteDocumentRequest,
|
||||
DeleteDocumentResponse,
|
||||
DocumentSummary,
|
||||
GetDocumentRequest,
|
||||
IngestDocumentRequest,
|
||||
IngestDocumentResponse,
|
||||
IngestionServiceClient,
|
||||
} from '../gen/apf-ai/ingestion';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { AI_INGESTION_GRPC_CLIENT } from './tokens';
|
||||
|
||||
/**
|
||||
* Wrapper around the generated `IngestionServiceClient`.
|
||||
*
|
||||
* Unused by the BFF in v1 — apf-ai-service ships a dedicated CLI
|
||||
* (`tools/Apf.Ai.Ingest/`) for document ingestion during the POC.
|
||||
* The wrapper is in place so the future admin "manage AI corpus"
|
||||
* surface lands as one new controller, not as a new module + a new
|
||||
* client + a new generated stub binding. The unit tests under
|
||||
* `ingestion.client.spec.ts` lock the wire contract today.
|
||||
*/
|
||||
@Injectable()
|
||||
export class IngestionClient {
|
||||
constructor(
|
||||
@Inject(AI_INGESTION_GRPC_CLIENT) private readonly grpc: IngestionServiceClient,
|
||||
private readonly metadata: GrpcMetadataBuilder,
|
||||
) {}
|
||||
|
||||
ingestDocument(
|
||||
request: IngestDocumentRequest,
|
||||
options: { correlationId?: string } = {},
|
||||
): Promise<IngestDocumentResponse> {
|
||||
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||
return new Promise<IngestDocumentResponse>((resolve, reject) => {
|
||||
this.grpc.ingestDocument(request, metadata, (err, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getDocument(
|
||||
request: GetDocumentRequest,
|
||||
options: { correlationId?: string } = {},
|
||||
): Promise<DocumentSummary> {
|
||||
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||
return new Promise<DocumentSummary>((resolve, reject) => {
|
||||
this.grpc.getDocument(request, metadata, (err, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteDocument(
|
||||
request: DeleteDocumentRequest,
|
||||
options: { correlationId?: string } = {},
|
||||
): Promise<DeleteDocumentResponse> {
|
||||
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||
return new Promise<DeleteDocumentResponse>((resolve, reject) => {
|
||||
this.grpc.deleteDocument(request, metadata, (err, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type {
|
||||
ListModelsRequest,
|
||||
ListModelsResponse,
|
||||
ModelsServiceClient,
|
||||
} from '../gen/apf-ai/models';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { AI_MODELS_GRPC_CLIENT } from './tokens';
|
||||
|
||||
/**
|
||||
* Wrapper around the generated `ModelsServiceClient` for the unary
|
||||
* `ListModels` RPC. Surfaces the active provider + the configured
|
||||
* provider catalogue for use by a future admin UI tile or a v1
|
||||
* "which models are available" health probe.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ModelsClient {
|
||||
constructor(
|
||||
@Inject(AI_MODELS_GRPC_CLIENT) private readonly grpc: ModelsServiceClient,
|
||||
private readonly metadata: GrpcMetadataBuilder,
|
||||
) {}
|
||||
|
||||
listModels(
|
||||
request: ListModelsRequest = {},
|
||||
options: { correlationId?: string } = {},
|
||||
): Promise<ListModelsResponse> {
|
||||
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||
return new Promise<ListModelsResponse>((resolve, reject) => {
|
||||
this.grpc.listModels(request, metadata, (err, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { describe, it, expect, beforeEach, afterAll } from '@jest/globals';
|
||||
import { HashUserIdService } from '../../audit/hash-user-id.service';
|
||||
import { PrincipalMapper } from './principal.mapper';
|
||||
|
||||
/**
|
||||
* Locks the Principal-mapping contract per ADR-0024 §"Sub-decision 4":
|
||||
*
|
||||
* - subject = HashUserIdService(oid) — same salt, same algo as the
|
||||
* portal's audit and Pino log streams. This is the
|
||||
* join key between portal and apf-ai-service audit
|
||||
* trails.
|
||||
* - roles = pass-through (inclusive expansion lands in a future
|
||||
* ADR; the wire contract on the AI side does not
|
||||
* change).
|
||||
* - attrs = tenantId is reserved; consumers can layer extra
|
||||
* key/value pairs but tenantId wins on collision.
|
||||
*/
|
||||
|
||||
const STRONG_SALT = randomBytes(32).toString('base64url');
|
||||
const ORIGINAL_SALT = process.env['LOG_USER_ID_SALT'];
|
||||
|
||||
beforeEach(() => {
|
||||
process.env['LOG_USER_ID_SALT'] = STRONG_SALT;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (ORIGINAL_SALT === undefined) {
|
||||
delete process.env['LOG_USER_ID_SALT'];
|
||||
} else {
|
||||
process.env['LOG_USER_ID_SALT'] = ORIGINAL_SALT;
|
||||
}
|
||||
});
|
||||
|
||||
function makeMapper(): { mapper: PrincipalMapper; hashService: HashUserIdService } {
|
||||
const hashService = new HashUserIdService();
|
||||
return { mapper: new PrincipalMapper(hashService), hashService };
|
||||
}
|
||||
|
||||
describe('PrincipalMapper', () => {
|
||||
it('hashes the Entra oid into the subject via HashUserIdService', () => {
|
||||
const { mapper, hashService } = makeMapper();
|
||||
const principal = mapper.fromInputs({
|
||||
oid: 'user-oid-42',
|
||||
tid: 'tenant-1',
|
||||
roles: ['admin'],
|
||||
});
|
||||
expect(principal.subject).toBe(hashService.hash('user-oid-42'));
|
||||
expect(principal.subject).toMatch(/^[0-9a-f]{16}$/);
|
||||
});
|
||||
|
||||
it('passes roles through verbatim (no expansion in v1)', () => {
|
||||
const { mapper } = makeMapper();
|
||||
const principal = mapper.fromInputs({
|
||||
oid: 'user-oid-42',
|
||||
tid: 'tenant-1',
|
||||
roles: ['directeur', 'rh'],
|
||||
});
|
||||
expect(principal.roles).toEqual(['directeur', 'rh']);
|
||||
});
|
||||
|
||||
it('copies roles so caller mutations do not leak into the proto', () => {
|
||||
const { mapper } = makeMapper();
|
||||
const roles: string[] = ['admin'];
|
||||
const principal = mapper.fromInputs({
|
||||
oid: 'user-oid-42',
|
||||
tid: 'tenant-1',
|
||||
roles,
|
||||
});
|
||||
roles.push('mutated');
|
||||
expect(principal.roles).toEqual(['admin']);
|
||||
});
|
||||
|
||||
it('always emits tenantId in attributes', () => {
|
||||
const { mapper } = makeMapper();
|
||||
const principal = mapper.fromInputs({
|
||||
oid: 'user-oid-42',
|
||||
tid: 'tenant-1',
|
||||
roles: [],
|
||||
});
|
||||
expect(principal.attributes).toEqual({ tenantId: 'tenant-1' });
|
||||
});
|
||||
|
||||
it('merges extraAttributes into the attribute map', () => {
|
||||
const { mapper } = makeMapper();
|
||||
const principal = mapper.fromInputs({
|
||||
oid: 'user-oid-42',
|
||||
tid: 'tenant-1',
|
||||
roles: [],
|
||||
extraAttributes: { delegation: 'aquitaine', region: 'fr-sw' },
|
||||
});
|
||||
expect(principal.attributes).toEqual({
|
||||
tenantId: 'tenant-1',
|
||||
delegation: 'aquitaine',
|
||||
region: 'fr-sw',
|
||||
});
|
||||
});
|
||||
|
||||
it('tenantId from `tid` wins over a colliding extraAttributes entry', () => {
|
||||
const { mapper } = makeMapper();
|
||||
const principal = mapper.fromInputs({
|
||||
oid: 'user-oid-42',
|
||||
tid: 'tenant-1',
|
||||
roles: [],
|
||||
extraAttributes: { tenantId: 'tenant-spoofed' },
|
||||
});
|
||||
expect(principal.attributes['tenantId']).toBe('tenant-1');
|
||||
});
|
||||
|
||||
it('produces the same subject for the same oid + salt across instances', () => {
|
||||
const { mapper: m1 } = makeMapper();
|
||||
const { mapper: m2 } = makeMapper();
|
||||
expect(m1.fromInputs({ oid: 'x', tid: 't', roles: [] }).subject).toBe(
|
||||
m2.fromInputs({ oid: 'x', tid: 't', roles: [] }).subject,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HashUserIdService } from '../../audit/hash-user-id.service';
|
||||
import type { Principal } from '../gen/apf-ai/common';
|
||||
|
||||
/**
|
||||
* Read-only view of the session fields the mapper consumes. Decoupled
|
||||
* from `AuthenticatedUser` so callers can build a Principal in tests
|
||||
* or in non-session contexts (a future background-job consumer) by
|
||||
* supplying a plain object — without dragging the full session shape
|
||||
* along.
|
||||
*
|
||||
* Keep the shape minimal: anything beyond `oid + tid + roles` should
|
||||
* land in `attributes` rather than as a new top-level field, so the
|
||||
* proto contract stays stable as the BFF grows new claim sources.
|
||||
*/
|
||||
export interface PrincipalInputs {
|
||||
/** Entra Object ID. Hashed via `HashUserIdService` into the proto `subject` field. */
|
||||
readonly oid: string;
|
||||
/** Entra tenant id. Surfaced as `attributes.tenantId`. */
|
||||
readonly tid: string;
|
||||
/** Roles already resolved upstream by the auth flow. */
|
||||
readonly roles: readonly string[];
|
||||
/**
|
||||
* Free-form extra attributes (delegation slug, region, …) merged
|
||||
* into `attributes` after `tenantId`. Caller-controlled; the
|
||||
* mapper does not enrich. Reserved keys (`tenantId`) overwrite
|
||||
* the caller-provided value to keep the proto contract single-
|
||||
* sourced.
|
||||
*/
|
||||
readonly extraAttributes?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the `apf.ai.v1.Principal` proto from a portal session,
|
||||
* per ADR-0024 §"Sub-decision 4 — unsigned Principal in the proto body".
|
||||
*
|
||||
* The `subject` field MUST be the same hash the audit module writes
|
||||
* to `audit.events.actor_id_hash` (per [ADR-0013](../../docs/decisions/0013-audit-trail-separated-postgres-append-only.md))
|
||||
* and that Pino's `user_id_hash` log field carries (per [ADR-0012](../../docs/decisions/0012-observability-pino-opentelemetry.md)).
|
||||
* The two services' audit trails only join cleanly when both sides
|
||||
* compute the same value — same Entra `oid`, same salt, same algo.
|
||||
* The mapper delegates to `HashUserIdService` to make that contract
|
||||
* explicit at the wire boundary.
|
||||
*
|
||||
* The `roles` field is passed through verbatim in v1. A future ADR
|
||||
* on role hierarchy (proposed alongside the stargate migration
|
||||
* analysis) will introduce inclusive expansion (`Admin` ⇒
|
||||
* `[admin, directeur, rh, collaborateur]`). At that point the
|
||||
* mapper grows a `RoleExpander` collaborator; the wire shape and
|
||||
* the upstream contract on the AI side stay unchanged.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PrincipalMapper {
|
||||
constructor(private readonly hashUserId: HashUserIdService) {}
|
||||
|
||||
fromInputs(inputs: PrincipalInputs): Principal {
|
||||
return {
|
||||
subject: this.hashUserId.hash(inputs.oid),
|
||||
roles: [...inputs.roles],
|
||||
attributes: {
|
||||
...(inputs.extraAttributes ?? {}),
|
||||
tenantId: inputs.tid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||
import {
|
||||
ChannelCredentials,
|
||||
Server,
|
||||
ServerCredentials,
|
||||
status as GrpcStatus,
|
||||
type Metadata,
|
||||
type sendUnaryData,
|
||||
type ServerUnaryCall,
|
||||
} from '@grpc/grpc-js';
|
||||
import {
|
||||
RagServiceClient,
|
||||
RagServiceService,
|
||||
type RagSearchRequest,
|
||||
type RagSearchResponse,
|
||||
} from '../gen/apf-ai/rag';
|
||||
import { Struct } from '../gen/apf-ai/google/protobuf/struct';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { RagClient } from './rag.client';
|
||||
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||
|
||||
/**
|
||||
* Locks the unary RAG contract: the wrapper promisifies the
|
||||
* callback-style stub, forwards metadata to the server, and
|
||||
* surfaces `ServiceError` rejections with the original gRPC status
|
||||
* code intact.
|
||||
*/
|
||||
|
||||
const CONFIG: AiServiceConfig = {
|
||||
endpoint: '',
|
||||
clientId: 'apf-portal-test',
|
||||
useTls: false,
|
||||
};
|
||||
|
||||
interface ServerObservations {
|
||||
request: RagSearchRequest | null;
|
||||
metadata: Metadata | null;
|
||||
}
|
||||
|
||||
let server: Server;
|
||||
let port: number;
|
||||
let serverObservations: ServerObservations;
|
||||
let searchHandler: (
|
||||
call: ServerUnaryCall<RagSearchRequest, RagSearchResponse>,
|
||||
callback: sendUnaryData<RagSearchResponse>,
|
||||
) => void;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = new Server();
|
||||
server.addService(RagServiceService, {
|
||||
search: (call, callback) => {
|
||||
serverObservations.request = call.request;
|
||||
serverObservations.metadata = call.metadata;
|
||||
searchHandler(call, callback);
|
||||
},
|
||||
});
|
||||
port = await new Promise<number>((resolve, reject) => {
|
||||
server.bindAsync('127.0.0.1:0', ServerCredentials.createInsecure(), (err, bound) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(bound);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.tryShutdown((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
serverObservations = { request: null, metadata: null };
|
||||
});
|
||||
|
||||
function buildClient(): RagClient {
|
||||
const config = { ...CONFIG, endpoint: `127.0.0.1:${port}` };
|
||||
const grpc = new RagServiceClient(config.endpoint, ChannelCredentials.createInsecure());
|
||||
return new RagClient(grpc, new GrpcMetadataBuilder(config));
|
||||
}
|
||||
|
||||
describe('RagClient (in-process fake server)', () => {
|
||||
it('resolves with the server response on a happy unary call', async () => {
|
||||
searchHandler = (_call, callback) => {
|
||||
callback(null, {
|
||||
chunks: [
|
||||
{
|
||||
id: 'chunk-1',
|
||||
documentId: 'doc-1',
|
||||
content: 'Lorem ipsum',
|
||||
source: 'rag-test',
|
||||
score: 0.99,
|
||||
metadata: Struct.fromPartial({}),
|
||||
},
|
||||
],
|
||||
correlationId: 'echo-corr',
|
||||
});
|
||||
};
|
||||
|
||||
const client = buildClient();
|
||||
const response = await client.search(
|
||||
{
|
||||
query: 'hello',
|
||||
topK: 3,
|
||||
principal: { subject: 'subj-1', roles: [], attributes: {} },
|
||||
filters: { source: '', documentId: '' },
|
||||
},
|
||||
{ correlationId: 'caller-corr' },
|
||||
);
|
||||
|
||||
expect(response.chunks).toHaveLength(1);
|
||||
expect(response.chunks[0]?.id).toBe('chunk-1');
|
||||
expect(serverObservations.metadata?.get('x-correlation-id')).toEqual(['caller-corr']);
|
||||
});
|
||||
|
||||
it('rejects with the original gRPC ServiceError on server error', async () => {
|
||||
searchHandler = (_call, callback) => {
|
||||
callback({
|
||||
code: GrpcStatus.PERMISSION_DENIED,
|
||||
details: 'forbidden',
|
||||
metadata: undefined as unknown as Metadata,
|
||||
name: 'Error',
|
||||
message: 'forbidden',
|
||||
});
|
||||
};
|
||||
|
||||
const client = buildClient();
|
||||
await expect(
|
||||
client.search({
|
||||
query: 'q',
|
||||
topK: 1,
|
||||
principal: { subject: 'subj-2', roles: [], attributes: {} },
|
||||
filters: { source: '', documentId: '' },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: GrpcStatus.PERMISSION_DENIED });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { RagSearchRequest, RagSearchResponse, RagServiceClient } from '../gen/apf-ai/rag';
|
||||
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||
import { AI_RAG_GRPC_CLIENT } from './tokens';
|
||||
|
||||
/**
|
||||
* Wrapper around the generated `RagServiceClient` for the unary
|
||||
* `Search` RPC, per ADR-0024.
|
||||
*
|
||||
* Promisifies the callback-style gRPC stub and injects the standard
|
||||
* metadata. Errors propagate as the underlying `ServiceError` (with
|
||||
* `code`, `details`, etc.) so callers can map them to portal-side
|
||||
* HTTP statuses without losing the gRPC status code.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RagClient {
|
||||
constructor(
|
||||
@Inject(AI_RAG_GRPC_CLIENT) private readonly grpc: RagServiceClient,
|
||||
private readonly metadata: GrpcMetadataBuilder,
|
||||
) {}
|
||||
|
||||
search(
|
||||
request: RagSearchRequest,
|
||||
options: { correlationId?: string } = {},
|
||||
): Promise<RagSearchResponse> {
|
||||
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||
return new Promise<RagSearchResponse>((resolve, reject) => {
|
||||
this.grpc.search(request, metadata, (err, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Dependency-injection tokens for `AiClientModule`, per ADR-0024.
|
||||
*
|
||||
* The module exposes three classes of provider:
|
||||
*
|
||||
* 1. `AI_CONFIG` — the parsed env vars from `assertAiServiceConfig`.
|
||||
* 2. `AI_CREDENTIALS` — the `ChannelCredentials` (insecure in dev
|
||||
* h2c, SSL in prod h2 + TLS).
|
||||
* 3. Generated gRPC stub instances — one per RPC service from
|
||||
* `apf.ai.v1.*` (Chat, Rag, Ingestion, Models). Wrapped by the
|
||||
* `*.client.ts` services in this folder; the raw stubs stay
|
||||
* behind tokens so the wrapper layer is the only place that
|
||||
* knows about the codegen output.
|
||||
*/
|
||||
|
||||
export const AI_CONFIG = Symbol('AI_CONFIG');
|
||||
export const AI_CREDENTIALS = Symbol('AI_CREDENTIALS');
|
||||
export const AI_CHAT_GRPC_CLIENT = Symbol('AI_CHAT_GRPC_CLIENT');
|
||||
export const AI_RAG_GRPC_CLIENT = Symbol('AI_RAG_GRPC_CLIENT');
|
||||
export const AI_INGESTION_GRPC_CLIENT = Symbol('AI_INGESTION_GRPC_CLIENT');
|
||||
export const AI_MODELS_GRPC_CLIENT = Symbol('AI_MODELS_GRPC_CLIENT');
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,765 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.8
|
||||
// protoc v3.19.1
|
||||
// source: common.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import Long from "long";
|
||||
import { Struct } from "./google/protobuf/struct";
|
||||
|
||||
export enum ChatRole {
|
||||
CHAT_ROLE_UNSPECIFIED = 0,
|
||||
CHAT_ROLE_SYSTEM = 1,
|
||||
CHAT_ROLE_USER = 2,
|
||||
CHAT_ROLE_ASSISTANT = 3,
|
||||
CHAT_ROLE_TOOL = 4,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function chatRoleFromJSON(object: any): ChatRole {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "CHAT_ROLE_UNSPECIFIED":
|
||||
return ChatRole.CHAT_ROLE_UNSPECIFIED;
|
||||
case 1:
|
||||
case "CHAT_ROLE_SYSTEM":
|
||||
return ChatRole.CHAT_ROLE_SYSTEM;
|
||||
case 2:
|
||||
case "CHAT_ROLE_USER":
|
||||
return ChatRole.CHAT_ROLE_USER;
|
||||
case 3:
|
||||
case "CHAT_ROLE_ASSISTANT":
|
||||
return ChatRole.CHAT_ROLE_ASSISTANT;
|
||||
case 4:
|
||||
case "CHAT_ROLE_TOOL":
|
||||
return ChatRole.CHAT_ROLE_TOOL;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return ChatRole.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function chatRoleToJSON(object: ChatRole): string {
|
||||
switch (object) {
|
||||
case ChatRole.CHAT_ROLE_UNSPECIFIED:
|
||||
return "CHAT_ROLE_UNSPECIFIED";
|
||||
case ChatRole.CHAT_ROLE_SYSTEM:
|
||||
return "CHAT_ROLE_SYSTEM";
|
||||
case ChatRole.CHAT_ROLE_USER:
|
||||
return "CHAT_ROLE_USER";
|
||||
case ChatRole.CHAT_ROLE_ASSISTANT:
|
||||
return "CHAT_ROLE_ASSISTANT";
|
||||
case ChatRole.CHAT_ROLE_TOOL:
|
||||
return "CHAT_ROLE_TOOL";
|
||||
case ChatRole.UNRECOGNIZED:
|
||||
default:
|
||||
return "UNRECOGNIZED";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity-bearing principal shipped with every request (carried in the JWT for
|
||||
* internal callers, or in the request body for API-key callers — same shape).
|
||||
*/
|
||||
export interface Principal {
|
||||
subject: string;
|
||||
roles: string[];
|
||||
attributes: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface Principal_AttributesEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default-deny ACL attached to every ingested chunk.
|
||||
* At least one of allowed_roles or allowed_subjects must be non-empty,
|
||||
* enforced at the controller, the DB CHECK constraint, and the post-filter.
|
||||
*/
|
||||
export interface AclSpec {
|
||||
allowedRoles: string[];
|
||||
allowedSubjects: string[];
|
||||
deniedRoles: string[];
|
||||
requiredAttributes: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface AclSpec_RequiredAttributesEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
toolCallId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON-schema descriptor for caller-side tool execution. The AI service is
|
||||
* tool-blind: it emits ToolCall events; the caller executes and posts back.
|
||||
*/
|
||||
export interface ToolDescriptor {
|
||||
name: string;
|
||||
description: string;
|
||||
schema?: { [key: string]: any } | undefined;
|
||||
}
|
||||
|
||||
function createBasePrincipal(): Principal {
|
||||
return { subject: "", roles: [], attributes: {} };
|
||||
}
|
||||
|
||||
export const Principal: MessageFns<Principal> = {
|
||||
encode(message: Principal, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.subject !== "") {
|
||||
writer.uint32(10).string(message.subject);
|
||||
}
|
||||
for (const v of message.roles) {
|
||||
writer.uint32(18).string(v!);
|
||||
}
|
||||
globalThis.Object.entries(message.attributes).forEach(([key, value]: [string, string]) => {
|
||||
Principal_AttributesEntry.encode({ key: key as any, value }, writer.uint32(26).fork()).join();
|
||||
});
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Principal {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePrincipal();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.subject = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.roles.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
const entry3 = Principal_AttributesEntry.decode(reader, reader.uint32());
|
||||
if (entry3.value !== undefined) {
|
||||
message.attributes[entry3.key] = entry3.value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Principal {
|
||||
return {
|
||||
subject: isSet(object.subject) ? globalThis.String(object.subject) : "",
|
||||
roles: globalThis.Array.isArray(object?.roles) ? object.roles.map((e: any) => globalThis.String(e)) : [],
|
||||
attributes: isObject(object.attributes)
|
||||
? (globalThis.Object.entries(object.attributes) as [string, any][]).reduce(
|
||||
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
|
||||
acc[key] = globalThis.String(value);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: {},
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Principal): unknown {
|
||||
const obj: any = {};
|
||||
if (message.subject !== "") {
|
||||
obj.subject = message.subject;
|
||||
}
|
||||
if (message.roles?.length) {
|
||||
obj.roles = message.roles;
|
||||
}
|
||||
if (message.attributes) {
|
||||
const entries = globalThis.Object.entries(message.attributes) as [string, string][];
|
||||
if (entries.length > 0) {
|
||||
obj.attributes = {};
|
||||
entries.forEach(([k, v]) => {
|
||||
obj.attributes[k] = v;
|
||||
});
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Principal>, I>>(base?: I): Principal {
|
||||
return Principal.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Principal>, I>>(object: I): Principal {
|
||||
const message = createBasePrincipal();
|
||||
message.subject = object.subject ?? "";
|
||||
message.roles = object.roles?.map((e) => e) || [];
|
||||
message.attributes = (globalThis.Object.entries(object.attributes ?? {}) as [string, string][]).reduce(
|
||||
(acc: { [key: string]: string }, [key, value]: [string, string]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = globalThis.String(value);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBasePrincipal_AttributesEntry(): Principal_AttributesEntry {
|
||||
return { key: "", value: "" };
|
||||
}
|
||||
|
||||
export const Principal_AttributesEntry: MessageFns<Principal_AttributesEntry> = {
|
||||
encode(message: Principal_AttributesEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.key !== "") {
|
||||
writer.uint32(10).string(message.key);
|
||||
}
|
||||
if (message.value !== "") {
|
||||
writer.uint32(18).string(message.value);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Principal_AttributesEntry {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePrincipal_AttributesEntry();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.key = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Principal_AttributesEntry {
|
||||
return {
|
||||
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Principal_AttributesEntry): unknown {
|
||||
const obj: any = {};
|
||||
if (message.key !== "") {
|
||||
obj.key = message.key;
|
||||
}
|
||||
if (message.value !== "") {
|
||||
obj.value = message.value;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Principal_AttributesEntry>, I>>(base?: I): Principal_AttributesEntry {
|
||||
return Principal_AttributesEntry.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Principal_AttributesEntry>, I>>(object: I): Principal_AttributesEntry {
|
||||
const message = createBasePrincipal_AttributesEntry();
|
||||
message.key = object.key ?? "";
|
||||
message.value = object.value ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAclSpec(): AclSpec {
|
||||
return { allowedRoles: [], allowedSubjects: [], deniedRoles: [], requiredAttributes: {} };
|
||||
}
|
||||
|
||||
export const AclSpec: MessageFns<AclSpec> = {
|
||||
encode(message: AclSpec, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.allowedRoles) {
|
||||
writer.uint32(10).string(v!);
|
||||
}
|
||||
for (const v of message.allowedSubjects) {
|
||||
writer.uint32(18).string(v!);
|
||||
}
|
||||
for (const v of message.deniedRoles) {
|
||||
writer.uint32(26).string(v!);
|
||||
}
|
||||
globalThis.Object.entries(message.requiredAttributes).forEach(([key, value]: [string, string]) => {
|
||||
AclSpec_RequiredAttributesEntry.encode({ key: key as any, value }, writer.uint32(34).fork()).join();
|
||||
});
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AclSpec {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAclSpec();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.allowedRoles.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.allowedSubjects.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.deniedRoles.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
const entry4 = AclSpec_RequiredAttributesEntry.decode(reader, reader.uint32());
|
||||
if (entry4.value !== undefined) {
|
||||
message.requiredAttributes[entry4.key] = entry4.value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AclSpec {
|
||||
return {
|
||||
allowedRoles: globalThis.Array.isArray(object?.allowedRoles)
|
||||
? object.allowedRoles.map((e: any) => globalThis.String(e))
|
||||
: globalThis.Array.isArray(object?.allowed_roles)
|
||||
? object.allowed_roles.map((e: any) => globalThis.String(e))
|
||||
: [],
|
||||
allowedSubjects: globalThis.Array.isArray(object?.allowedSubjects)
|
||||
? object.allowedSubjects.map((e: any) => globalThis.String(e))
|
||||
: globalThis.Array.isArray(object?.allowed_subjects)
|
||||
? object.allowed_subjects.map((e: any) => globalThis.String(e))
|
||||
: [],
|
||||
deniedRoles: globalThis.Array.isArray(object?.deniedRoles)
|
||||
? object.deniedRoles.map((e: any) => globalThis.String(e))
|
||||
: globalThis.Array.isArray(object?.denied_roles)
|
||||
? object.denied_roles.map((e: any) => globalThis.String(e))
|
||||
: [],
|
||||
requiredAttributes: isObject(object.requiredAttributes)
|
||||
? (globalThis.Object.entries(object.requiredAttributes) as [string, any][]).reduce(
|
||||
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
|
||||
acc[key] = globalThis.String(value);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: isObject(object.required_attributes)
|
||||
? (globalThis.Object.entries(object.required_attributes) as [string, any][]).reduce(
|
||||
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
|
||||
acc[key] = globalThis.String(value);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: {},
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: AclSpec): unknown {
|
||||
const obj: any = {};
|
||||
if (message.allowedRoles?.length) {
|
||||
obj.allowedRoles = message.allowedRoles;
|
||||
}
|
||||
if (message.allowedSubjects?.length) {
|
||||
obj.allowedSubjects = message.allowedSubjects;
|
||||
}
|
||||
if (message.deniedRoles?.length) {
|
||||
obj.deniedRoles = message.deniedRoles;
|
||||
}
|
||||
if (message.requiredAttributes) {
|
||||
const entries = globalThis.Object.entries(message.requiredAttributes) as [string, string][];
|
||||
if (entries.length > 0) {
|
||||
obj.requiredAttributes = {};
|
||||
entries.forEach(([k, v]) => {
|
||||
obj.requiredAttributes[k] = v;
|
||||
});
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AclSpec>, I>>(base?: I): AclSpec {
|
||||
return AclSpec.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AclSpec>, I>>(object: I): AclSpec {
|
||||
const message = createBaseAclSpec();
|
||||
message.allowedRoles = object.allowedRoles?.map((e) => e) || [];
|
||||
message.allowedSubjects = object.allowedSubjects?.map((e) => e) || [];
|
||||
message.deniedRoles = object.deniedRoles?.map((e) => e) || [];
|
||||
message.requiredAttributes = (globalThis.Object.entries(object.requiredAttributes ?? {}) as [string, string][])
|
||||
.reduce((acc: { [key: string]: string }, [key, value]: [string, string]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = globalThis.String(value);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAclSpec_RequiredAttributesEntry(): AclSpec_RequiredAttributesEntry {
|
||||
return { key: "", value: "" };
|
||||
}
|
||||
|
||||
export const AclSpec_RequiredAttributesEntry: MessageFns<AclSpec_RequiredAttributesEntry> = {
|
||||
encode(message: AclSpec_RequiredAttributesEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.key !== "") {
|
||||
writer.uint32(10).string(message.key);
|
||||
}
|
||||
if (message.value !== "") {
|
||||
writer.uint32(18).string(message.value);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AclSpec_RequiredAttributesEntry {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAclSpec_RequiredAttributesEntry();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.key = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AclSpec_RequiredAttributesEntry {
|
||||
return {
|
||||
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: AclSpec_RequiredAttributesEntry): unknown {
|
||||
const obj: any = {};
|
||||
if (message.key !== "") {
|
||||
obj.key = message.key;
|
||||
}
|
||||
if (message.value !== "") {
|
||||
obj.value = message.value;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AclSpec_RequiredAttributesEntry>, I>>(base?: I): AclSpec_RequiredAttributesEntry {
|
||||
return AclSpec_RequiredAttributesEntry.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AclSpec_RequiredAttributesEntry>, I>>(
|
||||
object: I,
|
||||
): AclSpec_RequiredAttributesEntry {
|
||||
const message = createBaseAclSpec_RequiredAttributesEntry();
|
||||
message.key = object.key ?? "";
|
||||
message.value = object.value ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseChatMessage(): ChatMessage {
|
||||
return { role: 0, content: "", toolCallId: "", name: "" };
|
||||
}
|
||||
|
||||
export const ChatMessage: MessageFns<ChatMessage> = {
|
||||
encode(message: ChatMessage, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.role !== 0) {
|
||||
writer.uint32(8).int32(message.role);
|
||||
}
|
||||
if (message.content !== "") {
|
||||
writer.uint32(18).string(message.content);
|
||||
}
|
||||
if (message.toolCallId !== "") {
|
||||
writer.uint32(26).string(message.toolCallId);
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(34).string(message.name);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ChatMessage {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseChatMessage();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.role = reader.int32() as any;
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.content = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.toolCallId = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ChatMessage {
|
||||
return {
|
||||
role: isSet(object.role) ? chatRoleFromJSON(object.role) : 0,
|
||||
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||
toolCallId: isSet(object.toolCallId)
|
||||
? globalThis.String(object.toolCallId)
|
||||
: isSet(object.tool_call_id)
|
||||
? globalThis.String(object.tool_call_id)
|
||||
: "",
|
||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ChatMessage): unknown {
|
||||
const obj: any = {};
|
||||
if (message.role !== 0) {
|
||||
obj.role = chatRoleToJSON(message.role);
|
||||
}
|
||||
if (message.content !== "") {
|
||||
obj.content = message.content;
|
||||
}
|
||||
if (message.toolCallId !== "") {
|
||||
obj.toolCallId = message.toolCallId;
|
||||
}
|
||||
if (message.name !== "") {
|
||||
obj.name = message.name;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ChatMessage>, I>>(base?: I): ChatMessage {
|
||||
return ChatMessage.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ChatMessage>, I>>(object: I): ChatMessage {
|
||||
const message = createBaseChatMessage();
|
||||
message.role = object.role ?? 0;
|
||||
message.content = object.content ?? "";
|
||||
message.toolCallId = object.toolCallId ?? "";
|
||||
message.name = object.name ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseToolDescriptor(): ToolDescriptor {
|
||||
return { name: "", description: "", schema: undefined };
|
||||
}
|
||||
|
||||
export const ToolDescriptor: MessageFns<ToolDescriptor> = {
|
||||
encode(message: ToolDescriptor, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.name !== "") {
|
||||
writer.uint32(10).string(message.name);
|
||||
}
|
||||
if (message.description !== "") {
|
||||
writer.uint32(18).string(message.description);
|
||||
}
|
||||
if (message.schema !== undefined) {
|
||||
Struct.encode(Struct.wrap(message.schema), writer.uint32(26).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ToolDescriptor {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseToolDescriptor();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.description = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schema = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ToolDescriptor {
|
||||
return {
|
||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||
description: isSet(object.description) ? globalThis.String(object.description) : "",
|
||||
schema: isObject(object.schema) ? object.schema : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ToolDescriptor): unknown {
|
||||
const obj: any = {};
|
||||
if (message.name !== "") {
|
||||
obj.name = message.name;
|
||||
}
|
||||
if (message.description !== "") {
|
||||
obj.description = message.description;
|
||||
}
|
||||
if (message.schema !== undefined) {
|
||||
obj.schema = message.schema;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ToolDescriptor>, I>>(base?: I): ToolDescriptor {
|
||||
return ToolDescriptor.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ToolDescriptor>, I>>(object: I): ToolDescriptor {
|
||||
const message = createBaseToolDescriptor();
|
||||
message.name = object.name ?? "";
|
||||
message.description = object.description ?? "";
|
||||
message.schema = object.schema ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.8
|
||||
// protoc v3.19.1
|
||||
// source: google/protobuf/struct.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import Long from "long";
|
||||
|
||||
/**
|
||||
* `NullValue` is a singleton enumeration to represent the null value for the
|
||||
* `Value` type union.
|
||||
*
|
||||
* The JSON representation for `NullValue` is JSON `null`.
|
||||
*/
|
||||
export enum NullValue {
|
||||
/** NULL_VALUE - Null value. */
|
||||
NULL_VALUE = 0,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function nullValueFromJSON(object: any): NullValue {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "NULL_VALUE":
|
||||
return NullValue.NULL_VALUE;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return NullValue.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function nullValueToJSON(object: NullValue): string {
|
||||
switch (object) {
|
||||
case NullValue.NULL_VALUE:
|
||||
return "NULL_VALUE";
|
||||
case NullValue.UNRECOGNIZED:
|
||||
default:
|
||||
return "UNRECOGNIZED";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `Struct` represents a structured data value, consisting of fields
|
||||
* which map to dynamically typed values. In some languages, `Struct`
|
||||
* might be supported by a native representation. For example, in
|
||||
* scripting languages like JS a struct is represented as an
|
||||
* object. The details of that representation are described together
|
||||
* with the proto support for the language.
|
||||
*
|
||||
* The JSON representation for `Struct` is JSON object.
|
||||
*/
|
||||
export interface Struct {
|
||||
/** Unordered map of dynamically typed values. */
|
||||
fields: { [key: string]: any | undefined };
|
||||
}
|
||||
|
||||
export interface Struct_FieldsEntry {
|
||||
key: string;
|
||||
value?: any | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `Value` represents a dynamically typed value which can be either
|
||||
* null, a number, a string, a boolean, a recursive struct value, or a
|
||||
* list of values. A producer of value is expected to set one of these
|
||||
* variants. Absence of any variant indicates an error.
|
||||
*
|
||||
* The JSON representation for `Value` is JSON value.
|
||||
*/
|
||||
export interface Value {
|
||||
/** Represents a null value. */
|
||||
nullValue?:
|
||||
| NullValue
|
||||
| undefined;
|
||||
/** Represents a double value. */
|
||||
numberValue?:
|
||||
| number
|
||||
| undefined;
|
||||
/** Represents a string value. */
|
||||
stringValue?:
|
||||
| string
|
||||
| undefined;
|
||||
/** Represents a boolean value. */
|
||||
boolValue?:
|
||||
| boolean
|
||||
| undefined;
|
||||
/** Represents a structured value. */
|
||||
structValue?:
|
||||
| { [key: string]: any }
|
||||
| undefined;
|
||||
/** Represents a repeated `Value`. */
|
||||
listValue?: Array<any> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ListValue` is a wrapper around a repeated field of values.
|
||||
*
|
||||
* The JSON representation for `ListValue` is JSON array.
|
||||
*/
|
||||
export interface ListValue {
|
||||
/** Repeated field of dynamically typed values. */
|
||||
values: any[];
|
||||
}
|
||||
|
||||
function createBaseStruct(): Struct {
|
||||
return { fields: {} };
|
||||
}
|
||||
|
||||
export const Struct: MessageFns<Struct> & StructWrapperFns = {
|
||||
encode(message: Struct, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
globalThis.Object.entries(message.fields).forEach(([key, value]: [string, any | undefined]) => {
|
||||
if (value !== undefined) {
|
||||
Struct_FieldsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
|
||||
}
|
||||
});
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Struct {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseStruct();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
const entry1 = Struct_FieldsEntry.decode(reader, reader.uint32());
|
||||
if (entry1.value !== undefined) {
|
||||
message.fields[entry1.key] = entry1.value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Struct {
|
||||
return {
|
||||
fields: isObject(object.fields)
|
||||
? (globalThis.Object.entries(object.fields) as [string, any][]).reduce(
|
||||
(acc: { [key: string]: any | undefined }, [key, value]: [string, any]) => {
|
||||
acc[key] = value as any | undefined;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: {},
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Struct): unknown {
|
||||
const obj: any = {};
|
||||
if (message.fields) {
|
||||
const entries = globalThis.Object.entries(message.fields) as [string, any | undefined][];
|
||||
if (entries.length > 0) {
|
||||
obj.fields = {};
|
||||
entries.forEach(([k, v]) => {
|
||||
obj.fields[k] = v;
|
||||
});
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Struct>, I>>(base?: I): Struct {
|
||||
return Struct.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Struct>, I>>(object: I): Struct {
|
||||
const message = createBaseStruct();
|
||||
message.fields = (globalThis.Object.entries(object.fields ?? {}) as [string, any | undefined][]).reduce(
|
||||
(acc: { [key: string]: any | undefined }, [key, value]: [string, any | undefined]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return message;
|
||||
},
|
||||
|
||||
wrap(object: { [key: string]: any } | undefined): Struct {
|
||||
const struct = createBaseStruct();
|
||||
|
||||
if (object !== undefined) {
|
||||
for (const key of globalThis.Object.keys(object)) {
|
||||
struct.fields[key] = object[key];
|
||||
}
|
||||
}
|
||||
return struct;
|
||||
},
|
||||
|
||||
unwrap(message: Struct): { [key: string]: any } {
|
||||
const object: { [key: string]: any } = {};
|
||||
if (message.fields) {
|
||||
for (const key of globalThis.Object.keys(message.fields)) {
|
||||
object[key] = message.fields[key];
|
||||
}
|
||||
}
|
||||
return object;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseStruct_FieldsEntry(): Struct_FieldsEntry {
|
||||
return { key: "", value: undefined };
|
||||
}
|
||||
|
||||
export const Struct_FieldsEntry: MessageFns<Struct_FieldsEntry> = {
|
||||
encode(message: Struct_FieldsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.key !== "") {
|
||||
writer.uint32(10).string(message.key);
|
||||
}
|
||||
if (message.value !== undefined) {
|
||||
Value.encode(Value.wrap(message.value), writer.uint32(18).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Struct_FieldsEntry {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseStruct_FieldsEntry();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.key = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = Value.unwrap(Value.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Struct_FieldsEntry {
|
||||
return {
|
||||
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||
value: isSet(object?.value) ? object.value : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Struct_FieldsEntry): unknown {
|
||||
const obj: any = {};
|
||||
if (message.key !== "") {
|
||||
obj.key = message.key;
|
||||
}
|
||||
if (message.value !== undefined) {
|
||||
obj.value = message.value;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Struct_FieldsEntry>, I>>(base?: I): Struct_FieldsEntry {
|
||||
return Struct_FieldsEntry.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Struct_FieldsEntry>, I>>(object: I): Struct_FieldsEntry {
|
||||
const message = createBaseStruct_FieldsEntry();
|
||||
message.key = object.key ?? "";
|
||||
message.value = object.value ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseValue(): Value {
|
||||
return {
|
||||
nullValue: undefined,
|
||||
numberValue: undefined,
|
||||
stringValue: undefined,
|
||||
boolValue: undefined,
|
||||
structValue: undefined,
|
||||
listValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const Value: MessageFns<Value> & AnyValueWrapperFns = {
|
||||
encode(message: Value, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.nullValue !== undefined) {
|
||||
writer.uint32(8).int32(message.nullValue);
|
||||
}
|
||||
if (message.numberValue !== undefined) {
|
||||
writer.uint32(17).double(message.numberValue);
|
||||
}
|
||||
if (message.stringValue !== undefined) {
|
||||
writer.uint32(26).string(message.stringValue);
|
||||
}
|
||||
if (message.boolValue !== undefined) {
|
||||
writer.uint32(32).bool(message.boolValue);
|
||||
}
|
||||
if (message.structValue !== undefined) {
|
||||
Struct.encode(Struct.wrap(message.structValue), writer.uint32(42).fork()).join();
|
||||
}
|
||||
if (message.listValue !== undefined) {
|
||||
ListValue.encode(ListValue.wrap(message.listValue), writer.uint32(50).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Value {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseValue();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.nullValue = reader.int32() as any;
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 17) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.numberValue = reader.double();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.stringValue = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.boolValue = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.structValue = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 50) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.listValue = ListValue.unwrap(ListValue.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Value {
|
||||
return {
|
||||
nullValue: isSet(object.nullValue)
|
||||
? nullValueFromJSON(object.nullValue)
|
||||
: isSet(object.null_value)
|
||||
? nullValueFromJSON(object.null_value)
|
||||
: undefined,
|
||||
numberValue: isSet(object.numberValue)
|
||||
? globalThis.Number(object.numberValue)
|
||||
: isSet(object.number_value)
|
||||
? globalThis.Number(object.number_value)
|
||||
: undefined,
|
||||
stringValue: isSet(object.stringValue)
|
||||
? globalThis.String(object.stringValue)
|
||||
: isSet(object.string_value)
|
||||
? globalThis.String(object.string_value)
|
||||
: undefined,
|
||||
boolValue: isSet(object.boolValue)
|
||||
? globalThis.Boolean(object.boolValue)
|
||||
: isSet(object.bool_value)
|
||||
? globalThis.Boolean(object.bool_value)
|
||||
: undefined,
|
||||
structValue: isObject(object.structValue)
|
||||
? object.structValue
|
||||
: isObject(object.struct_value)
|
||||
? object.struct_value
|
||||
: undefined,
|
||||
listValue: globalThis.Array.isArray(object.listValue)
|
||||
? [...object.listValue]
|
||||
: globalThis.Array.isArray(object.list_value)
|
||||
? [...object.list_value]
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Value): unknown {
|
||||
const obj: any = {};
|
||||
if (message.nullValue !== undefined) {
|
||||
obj.nullValue = nullValueToJSON(message.nullValue);
|
||||
}
|
||||
if (message.numberValue !== undefined) {
|
||||
obj.numberValue = message.numberValue;
|
||||
}
|
||||
if (message.stringValue !== undefined) {
|
||||
obj.stringValue = message.stringValue;
|
||||
}
|
||||
if (message.boolValue !== undefined) {
|
||||
obj.boolValue = message.boolValue;
|
||||
}
|
||||
if (message.structValue !== undefined) {
|
||||
obj.structValue = message.structValue;
|
||||
}
|
||||
if (message.listValue !== undefined) {
|
||||
obj.listValue = message.listValue;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Value>, I>>(base?: I): Value {
|
||||
return Value.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Value>, I>>(object: I): Value {
|
||||
const message = createBaseValue();
|
||||
message.nullValue = object.nullValue ?? undefined;
|
||||
message.numberValue = object.numberValue ?? undefined;
|
||||
message.stringValue = object.stringValue ?? undefined;
|
||||
message.boolValue = object.boolValue ?? undefined;
|
||||
message.structValue = object.structValue ?? undefined;
|
||||
message.listValue = object.listValue ?? undefined;
|
||||
return message;
|
||||
},
|
||||
|
||||
wrap(value: any): Value {
|
||||
const result = createBaseValue();
|
||||
if (value === null) {
|
||||
result.nullValue = NullValue.NULL_VALUE;
|
||||
} else if (typeof value === "boolean") {
|
||||
result.boolValue = value;
|
||||
} else if (typeof value === "number") {
|
||||
result.numberValue = value;
|
||||
} else if (typeof value === "string") {
|
||||
result.stringValue = value;
|
||||
} else if (globalThis.Array.isArray(value)) {
|
||||
result.listValue = value;
|
||||
} else if (typeof value === "object") {
|
||||
result.structValue = value;
|
||||
} else if (typeof value !== "undefined") {
|
||||
throw new globalThis.Error("Unsupported any value type: " + typeof value);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined {
|
||||
if (message.stringValue !== undefined) {
|
||||
return message.stringValue;
|
||||
} else if (message?.numberValue !== undefined) {
|
||||
return message.numberValue;
|
||||
} else if (message?.boolValue !== undefined) {
|
||||
return message.boolValue;
|
||||
} else if (message?.structValue !== undefined) {
|
||||
return message.structValue as any;
|
||||
} else if (message?.listValue !== undefined) {
|
||||
return message.listValue;
|
||||
} else if (message?.nullValue !== undefined) {
|
||||
return null;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseListValue(): ListValue {
|
||||
return { values: [] };
|
||||
}
|
||||
|
||||
export const ListValue: MessageFns<ListValue> & ListValueWrapperFns = {
|
||||
encode(message: ListValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.values) {
|
||||
Value.encode(Value.wrap(v!), writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ListValue {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseListValue();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.values.push(Value.unwrap(Value.decode(reader, reader.uint32())));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ListValue {
|
||||
return { values: globalThis.Array.isArray(object?.values) ? [...object.values] : [] };
|
||||
},
|
||||
|
||||
toJSON(message: ListValue): unknown {
|
||||
const obj: any = {};
|
||||
if (message.values?.length) {
|
||||
obj.values = message.values;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ListValue>, I>>(base?: I): ListValue {
|
||||
return ListValue.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ListValue>, I>>(object: I): ListValue {
|
||||
const message = createBaseListValue();
|
||||
message.values = object.values?.map((e) => e) || [];
|
||||
return message;
|
||||
},
|
||||
|
||||
wrap(array: Array<any> | undefined): ListValue {
|
||||
const result = createBaseListValue();
|
||||
result.values = array ?? [];
|
||||
return result;
|
||||
},
|
||||
|
||||
unwrap(message: ListValue): Array<any> {
|
||||
if (message?.hasOwnProperty("values") && globalThis.Array.isArray(message.values)) {
|
||||
return message.values;
|
||||
} else {
|
||||
return message as any;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
|
||||
interface StructWrapperFns {
|
||||
wrap(object: { [key: string]: any } | undefined): Struct;
|
||||
unwrap(message: Struct): { [key: string]: any };
|
||||
}
|
||||
|
||||
interface AnyValueWrapperFns {
|
||||
wrap(value: any): Value;
|
||||
unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined;
|
||||
}
|
||||
|
||||
interface ListValueWrapperFns {
|
||||
wrap(array: Array<any> | undefined): ListValue;
|
||||
unwrap(message: ListValue): Array<any>;
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.8
|
||||
// protoc v3.19.1
|
||||
// source: google/protobuf/timestamp.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import Long from "long";
|
||||
|
||||
/**
|
||||
* A Timestamp represents a point in time independent of any time zone or local
|
||||
* calendar, encoded as a count of seconds and fractions of seconds at
|
||||
* nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||
* January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||
* Gregorian calendar backwards to year one.
|
||||
*
|
||||
* All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
|
||||
* second table is needed for interpretation, using a [24-hour linear
|
||||
* smear](https://developers.google.com/time/smear).
|
||||
*
|
||||
* The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
|
||||
* restricting to that range, we ensure that we can convert to and from [RFC
|
||||
* 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* Example 1: Compute Timestamp from POSIX `time()`.
|
||||
*
|
||||
* Timestamp timestamp;
|
||||
* timestamp.set_seconds(time(NULL));
|
||||
* timestamp.set_nanos(0);
|
||||
*
|
||||
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||
*
|
||||
* struct timeval tv;
|
||||
* gettimeofday(&tv, NULL);
|
||||
*
|
||||
* Timestamp timestamp;
|
||||
* timestamp.set_seconds(tv.tv_sec);
|
||||
* timestamp.set_nanos(tv.tv_usec * 1000);
|
||||
*
|
||||
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||
*
|
||||
* FILETIME ft;
|
||||
* GetSystemTimeAsFileTime(&ft);
|
||||
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||
*
|
||||
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||
* Timestamp timestamp;
|
||||
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||
*
|
||||
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||
*
|
||||
* long millis = System.currentTimeMillis();
|
||||
*
|
||||
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||
* .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||
*
|
||||
* Example 5: Compute Timestamp from Java `Instant.now()`.
|
||||
*
|
||||
* Instant now = Instant.now();
|
||||
*
|
||||
* Timestamp timestamp =
|
||||
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||
* .setNanos(now.getNano()).build();
|
||||
*
|
||||
* Example 6: Compute Timestamp from current time in Python.
|
||||
*
|
||||
* timestamp = Timestamp()
|
||||
* timestamp.GetCurrentTime()
|
||||
*
|
||||
* # JSON Mapping
|
||||
*
|
||||
* In JSON format, the Timestamp type is encoded as a string in the
|
||||
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||
* where {year} is always expressed using four digits while {month}, {day},
|
||||
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||
* is required. A proto3 JSON serializer should always use UTC (as indicated by
|
||||
* "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||
* able to accept both UTC and other timezones (as indicated by an offset).
|
||||
*
|
||||
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||
* 01:30 UTC on January 15, 2017.
|
||||
*
|
||||
* In JavaScript, one can convert a Date object to this format using the
|
||||
* standard
|
||||
* [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||
* method. In Python, a standard `datetime.datetime` object can be converted
|
||||
* to this format using
|
||||
* [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
|
||||
* the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
|
||||
* the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||
* http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D
|
||||
* ) to obtain a formatter capable of generating timestamps in this format.
|
||||
*/
|
||||
export interface Timestamp {
|
||||
/**
|
||||
* Represents seconds of UTC time since Unix epoch
|
||||
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||
* 9999-12-31T23:59:59Z inclusive.
|
||||
*/
|
||||
seconds: Long;
|
||||
/**
|
||||
* Non-negative fractions of a second at nanosecond resolution. Negative
|
||||
* second values with fractions must still have non-negative nanos values
|
||||
* that count forward in time. Must be from 0 to 999,999,999
|
||||
* inclusive.
|
||||
*/
|
||||
nanos: number;
|
||||
}
|
||||
|
||||
function createBaseTimestamp(): Timestamp {
|
||||
return { seconds: Long.ZERO, nanos: 0 };
|
||||
}
|
||||
|
||||
export const Timestamp: MessageFns<Timestamp> = {
|
||||
encode(message: Timestamp, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (!message.seconds.equals(Long.ZERO)) {
|
||||
writer.uint32(8).int64(message.seconds.toString());
|
||||
}
|
||||
if (message.nanos !== 0) {
|
||||
writer.uint32(16).int32(message.nanos);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Timestamp {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseTimestamp();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.seconds = Long.fromString(reader.int64().toString());
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.nanos = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Timestamp {
|
||||
return {
|
||||
seconds: isSet(object.seconds) ? Long.fromValue(object.seconds) : Long.ZERO,
|
||||
nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Timestamp): unknown {
|
||||
const obj: any = {};
|
||||
if (!message.seconds.equals(Long.ZERO)) {
|
||||
obj.seconds = (message.seconds || Long.ZERO).toString();
|
||||
}
|
||||
if (message.nanos !== 0) {
|
||||
obj.nanos = Math.round(message.nanos);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Timestamp>, I>>(base?: I): Timestamp {
|
||||
return Timestamp.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Timestamp>, I>>(object: I): Timestamp {
|
||||
const message = createBaseTimestamp();
|
||||
message.seconds = (object.seconds !== undefined && object.seconds !== null)
|
||||
? Long.fromValue(object.seconds)
|
||||
: Long.ZERO;
|
||||
message.nanos = object.nanos ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -0,0 +1,858 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.8
|
||||
// protoc v3.19.1
|
||||
// source: ingestion.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
type CallOptions,
|
||||
type ChannelCredentials,
|
||||
Client,
|
||||
type ClientOptions,
|
||||
type ClientUnaryCall,
|
||||
type handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
type Metadata,
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import Long from "long";
|
||||
import { AclSpec, Principal } from "./common";
|
||||
import { Struct } from "./google/protobuf/struct";
|
||||
import { Timestamp } from "./google/protobuf/timestamp";
|
||||
|
||||
export interface IngestChunk {
|
||||
content: string;
|
||||
metadata?: { [key: string]: any } | undefined;
|
||||
acl?: AclSpec | undefined;
|
||||
}
|
||||
|
||||
export interface IngestDocumentRequest {
|
||||
source: string;
|
||||
externalId: string;
|
||||
title: string;
|
||||
chunks: IngestChunk[];
|
||||
principal?: Principal | undefined;
|
||||
}
|
||||
|
||||
export interface IngestDocumentResponse {
|
||||
documentId: string;
|
||||
chunksIngested: number;
|
||||
}
|
||||
|
||||
export interface GetDocumentRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DocumentSummary {
|
||||
id: string;
|
||||
sourceUri: string;
|
||||
title: string;
|
||||
externalId: string;
|
||||
ingestedAt?: Date | undefined;
|
||||
chunkCount: number;
|
||||
}
|
||||
|
||||
export interface DeleteDocumentRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DeleteDocumentResponse {
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
function createBaseIngestChunk(): IngestChunk {
|
||||
return { content: "", metadata: undefined, acl: undefined };
|
||||
}
|
||||
|
||||
export const IngestChunk: MessageFns<IngestChunk> = {
|
||||
encode(message: IngestChunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.content !== "") {
|
||||
writer.uint32(10).string(message.content);
|
||||
}
|
||||
if (message.metadata !== undefined) {
|
||||
Struct.encode(Struct.wrap(message.metadata), writer.uint32(18).fork()).join();
|
||||
}
|
||||
if (message.acl !== undefined) {
|
||||
AclSpec.encode(message.acl, writer.uint32(26).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): IngestChunk {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseIngestChunk();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.content = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.metadata = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.acl = AclSpec.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): IngestChunk {
|
||||
return {
|
||||
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||
metadata: isObject(object.metadata) ? object.metadata : undefined,
|
||||
acl: isSet(object.acl) ? AclSpec.fromJSON(object.acl) : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: IngestChunk): unknown {
|
||||
const obj: any = {};
|
||||
if (message.content !== "") {
|
||||
obj.content = message.content;
|
||||
}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = message.metadata;
|
||||
}
|
||||
if (message.acl !== undefined) {
|
||||
obj.acl = AclSpec.toJSON(message.acl);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<IngestChunk>, I>>(base?: I): IngestChunk {
|
||||
return IngestChunk.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<IngestChunk>, I>>(object: I): IngestChunk {
|
||||
const message = createBaseIngestChunk();
|
||||
message.content = object.content ?? "";
|
||||
message.metadata = object.metadata ?? undefined;
|
||||
message.acl = (object.acl !== undefined && object.acl !== null) ? AclSpec.fromPartial(object.acl) : undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseIngestDocumentRequest(): IngestDocumentRequest {
|
||||
return { source: "", externalId: "", title: "", chunks: [], principal: undefined };
|
||||
}
|
||||
|
||||
export const IngestDocumentRequest: MessageFns<IngestDocumentRequest> = {
|
||||
encode(message: IngestDocumentRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.source !== "") {
|
||||
writer.uint32(10).string(message.source);
|
||||
}
|
||||
if (message.externalId !== "") {
|
||||
writer.uint32(18).string(message.externalId);
|
||||
}
|
||||
if (message.title !== "") {
|
||||
writer.uint32(26).string(message.title);
|
||||
}
|
||||
for (const v of message.chunks) {
|
||||
IngestChunk.encode(v!, writer.uint32(34).fork()).join();
|
||||
}
|
||||
if (message.principal !== undefined) {
|
||||
Principal.encode(message.principal, writer.uint32(42).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): IngestDocumentRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseIngestDocumentRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.source = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.externalId = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.title = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.chunks.push(IngestChunk.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.principal = Principal.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): IngestDocumentRequest {
|
||||
return {
|
||||
source: isSet(object.source) ? globalThis.String(object.source) : "",
|
||||
externalId: isSet(object.externalId)
|
||||
? globalThis.String(object.externalId)
|
||||
: isSet(object.external_id)
|
||||
? globalThis.String(object.external_id)
|
||||
: "",
|
||||
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||
chunks: globalThis.Array.isArray(object?.chunks) ? object.chunks.map((e: any) => IngestChunk.fromJSON(e)) : [],
|
||||
principal: isSet(object.principal) ? Principal.fromJSON(object.principal) : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: IngestDocumentRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.source !== "") {
|
||||
obj.source = message.source;
|
||||
}
|
||||
if (message.externalId !== "") {
|
||||
obj.externalId = message.externalId;
|
||||
}
|
||||
if (message.title !== "") {
|
||||
obj.title = message.title;
|
||||
}
|
||||
if (message.chunks?.length) {
|
||||
obj.chunks = message.chunks.map((e) => IngestChunk.toJSON(e));
|
||||
}
|
||||
if (message.principal !== undefined) {
|
||||
obj.principal = Principal.toJSON(message.principal);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<IngestDocumentRequest>, I>>(base?: I): IngestDocumentRequest {
|
||||
return IngestDocumentRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<IngestDocumentRequest>, I>>(object: I): IngestDocumentRequest {
|
||||
const message = createBaseIngestDocumentRequest();
|
||||
message.source = object.source ?? "";
|
||||
message.externalId = object.externalId ?? "";
|
||||
message.title = object.title ?? "";
|
||||
message.chunks = object.chunks?.map((e) => IngestChunk.fromPartial(e)) || [];
|
||||
message.principal = (object.principal !== undefined && object.principal !== null)
|
||||
? Principal.fromPartial(object.principal)
|
||||
: undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseIngestDocumentResponse(): IngestDocumentResponse {
|
||||
return { documentId: "", chunksIngested: 0 };
|
||||
}
|
||||
|
||||
export const IngestDocumentResponse: MessageFns<IngestDocumentResponse> = {
|
||||
encode(message: IngestDocumentResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.documentId !== "") {
|
||||
writer.uint32(10).string(message.documentId);
|
||||
}
|
||||
if (message.chunksIngested !== 0) {
|
||||
writer.uint32(16).int32(message.chunksIngested);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): IngestDocumentResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseIngestDocumentResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.documentId = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.chunksIngested = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): IngestDocumentResponse {
|
||||
return {
|
||||
documentId: isSet(object.documentId)
|
||||
? globalThis.String(object.documentId)
|
||||
: isSet(object.document_id)
|
||||
? globalThis.String(object.document_id)
|
||||
: "",
|
||||
chunksIngested: isSet(object.chunksIngested)
|
||||
? globalThis.Number(object.chunksIngested)
|
||||
: isSet(object.chunks_ingested)
|
||||
? globalThis.Number(object.chunks_ingested)
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: IngestDocumentResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.documentId !== "") {
|
||||
obj.documentId = message.documentId;
|
||||
}
|
||||
if (message.chunksIngested !== 0) {
|
||||
obj.chunksIngested = Math.round(message.chunksIngested);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<IngestDocumentResponse>, I>>(base?: I): IngestDocumentResponse {
|
||||
return IngestDocumentResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<IngestDocumentResponse>, I>>(object: I): IngestDocumentResponse {
|
||||
const message = createBaseIngestDocumentResponse();
|
||||
message.documentId = object.documentId ?? "";
|
||||
message.chunksIngested = object.chunksIngested ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseGetDocumentRequest(): GetDocumentRequest {
|
||||
return { id: "" };
|
||||
}
|
||||
|
||||
export const GetDocumentRequest: MessageFns<GetDocumentRequest> = {
|
||||
encode(message: GetDocumentRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): GetDocumentRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseGetDocumentRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): GetDocumentRequest {
|
||||
return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: GetDocumentRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<GetDocumentRequest>, I>>(base?: I): GetDocumentRequest {
|
||||
return GetDocumentRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<GetDocumentRequest>, I>>(object: I): GetDocumentRequest {
|
||||
const message = createBaseGetDocumentRequest();
|
||||
message.id = object.id ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseDocumentSummary(): DocumentSummary {
|
||||
return { id: "", sourceUri: "", title: "", externalId: "", ingestedAt: undefined, chunkCount: 0 };
|
||||
}
|
||||
|
||||
export const DocumentSummary: MessageFns<DocumentSummary> = {
|
||||
encode(message: DocumentSummary, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.sourceUri !== "") {
|
||||
writer.uint32(18).string(message.sourceUri);
|
||||
}
|
||||
if (message.title !== "") {
|
||||
writer.uint32(26).string(message.title);
|
||||
}
|
||||
if (message.externalId !== "") {
|
||||
writer.uint32(34).string(message.externalId);
|
||||
}
|
||||
if (message.ingestedAt !== undefined) {
|
||||
Timestamp.encode(toTimestamp(message.ingestedAt), writer.uint32(42).fork()).join();
|
||||
}
|
||||
if (message.chunkCount !== 0) {
|
||||
writer.uint32(48).int32(message.chunkCount);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DocumentSummary {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDocumentSummary();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.sourceUri = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.title = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.externalId = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.ingestedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.chunkCount = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): DocumentSummary {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
sourceUri: isSet(object.sourceUri)
|
||||
? globalThis.String(object.sourceUri)
|
||||
: isSet(object.source_uri)
|
||||
? globalThis.String(object.source_uri)
|
||||
: "",
|
||||
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||
externalId: isSet(object.externalId)
|
||||
? globalThis.String(object.externalId)
|
||||
: isSet(object.external_id)
|
||||
? globalThis.String(object.external_id)
|
||||
: "",
|
||||
ingestedAt: isSet(object.ingestedAt)
|
||||
? fromJsonTimestamp(object.ingestedAt)
|
||||
: isSet(object.ingested_at)
|
||||
? fromJsonTimestamp(object.ingested_at)
|
||||
: undefined,
|
||||
chunkCount: isSet(object.chunkCount)
|
||||
? globalThis.Number(object.chunkCount)
|
||||
: isSet(object.chunk_count)
|
||||
? globalThis.Number(object.chunk_count)
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: DocumentSummary): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.sourceUri !== "") {
|
||||
obj.sourceUri = message.sourceUri;
|
||||
}
|
||||
if (message.title !== "") {
|
||||
obj.title = message.title;
|
||||
}
|
||||
if (message.externalId !== "") {
|
||||
obj.externalId = message.externalId;
|
||||
}
|
||||
if (message.ingestedAt !== undefined) {
|
||||
obj.ingestedAt = message.ingestedAt.toISOString();
|
||||
}
|
||||
if (message.chunkCount !== 0) {
|
||||
obj.chunkCount = Math.round(message.chunkCount);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DocumentSummary>, I>>(base?: I): DocumentSummary {
|
||||
return DocumentSummary.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<DocumentSummary>, I>>(object: I): DocumentSummary {
|
||||
const message = createBaseDocumentSummary();
|
||||
message.id = object.id ?? "";
|
||||
message.sourceUri = object.sourceUri ?? "";
|
||||
message.title = object.title ?? "";
|
||||
message.externalId = object.externalId ?? "";
|
||||
message.ingestedAt = object.ingestedAt ?? undefined;
|
||||
message.chunkCount = object.chunkCount ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseDeleteDocumentRequest(): DeleteDocumentRequest {
|
||||
return { id: "" };
|
||||
}
|
||||
|
||||
export const DeleteDocumentRequest: MessageFns<DeleteDocumentRequest> = {
|
||||
encode(message: DeleteDocumentRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteDocumentRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteDocumentRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeleteDocumentRequest {
|
||||
return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: DeleteDocumentRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteDocumentRequest>, I>>(base?: I): DeleteDocumentRequest {
|
||||
return DeleteDocumentRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteDocumentRequest>, I>>(object: I): DeleteDocumentRequest {
|
||||
const message = createBaseDeleteDocumentRequest();
|
||||
message.id = object.id ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseDeleteDocumentResponse(): DeleteDocumentResponse {
|
||||
return { deleted: false };
|
||||
}
|
||||
|
||||
export const DeleteDocumentResponse: MessageFns<DeleteDocumentResponse> = {
|
||||
encode(message: DeleteDocumentResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.deleted !== false) {
|
||||
writer.uint32(8).bool(message.deleted);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteDocumentResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteDocumentResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.deleted = reader.bool();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeleteDocumentResponse {
|
||||
return { deleted: isSet(object.deleted) ? globalThis.Boolean(object.deleted) : false };
|
||||
},
|
||||
|
||||
toJSON(message: DeleteDocumentResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.deleted !== false) {
|
||||
obj.deleted = message.deleted;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteDocumentResponse>, I>>(base?: I): DeleteDocumentResponse {
|
||||
return DeleteDocumentResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteDocumentResponse>, I>>(object: I): DeleteDocumentResponse {
|
||||
const message = createBaseDeleteDocumentResponse();
|
||||
message.deleted = object.deleted ?? false;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Document ingestion. Every chunk must carry an explicit ACL: empty
|
||||
* ACL is default-deny and rejected with InvalidArgument (gRPC) /
|
||||
* 422 (HTTP).
|
||||
*/
|
||||
export type IngestionServiceService = typeof IngestionServiceService;
|
||||
export const IngestionServiceService = {
|
||||
ingestDocument: {
|
||||
path: "/apf.ai.v1.IngestionService/IngestDocument" as const,
|
||||
requestStream: false as const,
|
||||
responseStream: false as const,
|
||||
requestSerialize: (value: IngestDocumentRequest): Buffer =>
|
||||
Buffer.from(IngestDocumentRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): IngestDocumentRequest => IngestDocumentRequest.decode(value),
|
||||
responseSerialize: (value: IngestDocumentResponse): Buffer =>
|
||||
Buffer.from(IngestDocumentResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): IngestDocumentResponse => IngestDocumentResponse.decode(value),
|
||||
},
|
||||
getDocument: {
|
||||
path: "/apf.ai.v1.IngestionService/GetDocument" as const,
|
||||
requestStream: false as const,
|
||||
responseStream: false as const,
|
||||
requestSerialize: (value: GetDocumentRequest): Buffer => Buffer.from(GetDocumentRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): GetDocumentRequest => GetDocumentRequest.decode(value),
|
||||
responseSerialize: (value: DocumentSummary): Buffer => Buffer.from(DocumentSummary.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): DocumentSummary => DocumentSummary.decode(value),
|
||||
},
|
||||
deleteDocument: {
|
||||
path: "/apf.ai.v1.IngestionService/DeleteDocument" as const,
|
||||
requestStream: false as const,
|
||||
responseStream: false as const,
|
||||
requestSerialize: (value: DeleteDocumentRequest): Buffer =>
|
||||
Buffer.from(DeleteDocumentRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): DeleteDocumentRequest => DeleteDocumentRequest.decode(value),
|
||||
responseSerialize: (value: DeleteDocumentResponse): Buffer =>
|
||||
Buffer.from(DeleteDocumentResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): DeleteDocumentResponse => DeleteDocumentResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface IngestionServiceServer extends UntypedServiceImplementation {
|
||||
ingestDocument: handleUnaryCall<IngestDocumentRequest, IngestDocumentResponse>;
|
||||
getDocument: handleUnaryCall<GetDocumentRequest, DocumentSummary>;
|
||||
deleteDocument: handleUnaryCall<DeleteDocumentRequest, DeleteDocumentResponse>;
|
||||
}
|
||||
|
||||
export interface IngestionServiceClient extends Client {
|
||||
ingestDocument(
|
||||
request: IngestDocumentRequest,
|
||||
callback: (error: ServiceError | null, response: IngestDocumentResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
ingestDocument(
|
||||
request: IngestDocumentRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: IngestDocumentResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
ingestDocument(
|
||||
request: IngestDocumentRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: IngestDocumentResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
getDocument(
|
||||
request: GetDocumentRequest,
|
||||
callback: (error: ServiceError | null, response: DocumentSummary) => void,
|
||||
): ClientUnaryCall;
|
||||
getDocument(
|
||||
request: GetDocumentRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: DocumentSummary) => void,
|
||||
): ClientUnaryCall;
|
||||
getDocument(
|
||||
request: GetDocumentRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: DocumentSummary) => void,
|
||||
): ClientUnaryCall;
|
||||
deleteDocument(
|
||||
request: DeleteDocumentRequest,
|
||||
callback: (error: ServiceError | null, response: DeleteDocumentResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
deleteDocument(
|
||||
request: DeleteDocumentRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: DeleteDocumentResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
deleteDocument(
|
||||
request: DeleteDocumentRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: DeleteDocumentResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
}
|
||||
|
||||
export const IngestionServiceClient = makeGenericClientConstructor(
|
||||
IngestionServiceService,
|
||||
"apf.ai.v1.IngestionService",
|
||||
) as unknown as {
|
||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): IngestionServiceClient;
|
||||
service: typeof IngestionServiceService;
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function toTimestamp(date: Date): Timestamp {
|
||||
const seconds = numberToLong(Math.trunc(date.getTime() / 1_000));
|
||||
const nanos = (date.getTime() % 1_000) * 1_000_000;
|
||||
return { seconds, nanos };
|
||||
}
|
||||
|
||||
function fromTimestamp(t: Timestamp): Date {
|
||||
let millis = (t.seconds.toNumber() || 0) * 1_000;
|
||||
millis += (t.nanos || 0) / 1_000_000;
|
||||
return new globalThis.Date(millis);
|
||||
}
|
||||
|
||||
function fromJsonTimestamp(o: any): Date {
|
||||
if (o instanceof globalThis.Date) {
|
||||
return o;
|
||||
} else if (typeof o === "string") {
|
||||
return new globalThis.Date(o);
|
||||
} else {
|
||||
return fromTimestamp(Timestamp.fromJSON(o));
|
||||
}
|
||||
}
|
||||
|
||||
function numberToLong(number: number) {
|
||||
return Long.fromNumber(number);
|
||||
}
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.8
|
||||
// protoc v3.19.1
|
||||
// source: models.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
type CallOptions,
|
||||
type ChannelCredentials,
|
||||
Client,
|
||||
type ClientOptions,
|
||||
type ClientUnaryCall,
|
||||
type handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
type Metadata,
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import Long from "long";
|
||||
|
||||
export interface ListModelsRequest {
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
discriminator: string;
|
||||
capabilities: string;
|
||||
endpoint: string;
|
||||
model: string;
|
||||
embeddingModel: string;
|
||||
}
|
||||
|
||||
export interface ListModelsResponse {
|
||||
active: string;
|
||||
providers: ProviderInfo[];
|
||||
}
|
||||
|
||||
function createBaseListModelsRequest(): ListModelsRequest {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const ListModelsRequest: MessageFns<ListModelsRequest> = {
|
||||
encode(_: ListModelsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ListModelsRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseListModelsRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(_: any): ListModelsRequest {
|
||||
return {};
|
||||
},
|
||||
|
||||
toJSON(_: ListModelsRequest): unknown {
|
||||
const obj: any = {};
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ListModelsRequest>, I>>(base?: I): ListModelsRequest {
|
||||
return ListModelsRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ListModelsRequest>, I>>(_: I): ListModelsRequest {
|
||||
const message = createBaseListModelsRequest();
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseProviderInfo(): ProviderInfo {
|
||||
return { discriminator: "", capabilities: "", endpoint: "", model: "", embeddingModel: "" };
|
||||
}
|
||||
|
||||
export const ProviderInfo: MessageFns<ProviderInfo> = {
|
||||
encode(message: ProviderInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.discriminator !== "") {
|
||||
writer.uint32(10).string(message.discriminator);
|
||||
}
|
||||
if (message.capabilities !== "") {
|
||||
writer.uint32(18).string(message.capabilities);
|
||||
}
|
||||
if (message.endpoint !== "") {
|
||||
writer.uint32(26).string(message.endpoint);
|
||||
}
|
||||
if (message.model !== "") {
|
||||
writer.uint32(34).string(message.model);
|
||||
}
|
||||
if (message.embeddingModel !== "") {
|
||||
writer.uint32(42).string(message.embeddingModel);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ProviderInfo {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseProviderInfo();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.discriminator = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.capabilities = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.endpoint = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.model = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.embeddingModel = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ProviderInfo {
|
||||
return {
|
||||
discriminator: isSet(object.discriminator) ? globalThis.String(object.discriminator) : "",
|
||||
capabilities: isSet(object.capabilities) ? globalThis.String(object.capabilities) : "",
|
||||
endpoint: isSet(object.endpoint) ? globalThis.String(object.endpoint) : "",
|
||||
model: isSet(object.model) ? globalThis.String(object.model) : "",
|
||||
embeddingModel: isSet(object.embeddingModel)
|
||||
? globalThis.String(object.embeddingModel)
|
||||
: isSet(object.embedding_model)
|
||||
? globalThis.String(object.embedding_model)
|
||||
: "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ProviderInfo): unknown {
|
||||
const obj: any = {};
|
||||
if (message.discriminator !== "") {
|
||||
obj.discriminator = message.discriminator;
|
||||
}
|
||||
if (message.capabilities !== "") {
|
||||
obj.capabilities = message.capabilities;
|
||||
}
|
||||
if (message.endpoint !== "") {
|
||||
obj.endpoint = message.endpoint;
|
||||
}
|
||||
if (message.model !== "") {
|
||||
obj.model = message.model;
|
||||
}
|
||||
if (message.embeddingModel !== "") {
|
||||
obj.embeddingModel = message.embeddingModel;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ProviderInfo>, I>>(base?: I): ProviderInfo {
|
||||
return ProviderInfo.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ProviderInfo>, I>>(object: I): ProviderInfo {
|
||||
const message = createBaseProviderInfo();
|
||||
message.discriminator = object.discriminator ?? "";
|
||||
message.capabilities = object.capabilities ?? "";
|
||||
message.endpoint = object.endpoint ?? "";
|
||||
message.model = object.model ?? "";
|
||||
message.embeddingModel = object.embeddingModel ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseListModelsResponse(): ListModelsResponse {
|
||||
return { active: "", providers: [] };
|
||||
}
|
||||
|
||||
export const ListModelsResponse: MessageFns<ListModelsResponse> = {
|
||||
encode(message: ListModelsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.active !== "") {
|
||||
writer.uint32(10).string(message.active);
|
||||
}
|
||||
for (const v of message.providers) {
|
||||
ProviderInfo.encode(v!, writer.uint32(18).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ListModelsResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseListModelsResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.active = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.providers.push(ProviderInfo.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ListModelsResponse {
|
||||
return {
|
||||
active: isSet(object.active) ? globalThis.String(object.active) : "",
|
||||
providers: globalThis.Array.isArray(object?.providers)
|
||||
? object.providers.map((e: any) => ProviderInfo.fromJSON(e))
|
||||
: [],
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ListModelsResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.active !== "") {
|
||||
obj.active = message.active;
|
||||
}
|
||||
if (message.providers?.length) {
|
||||
obj.providers = message.providers.map((e) => ProviderInfo.toJSON(e));
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ListModelsResponse>, I>>(base?: I): ListModelsResponse {
|
||||
return ListModelsResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ListModelsResponse>, I>>(object: I): ListModelsResponse {
|
||||
const message = createBaseListModelsResponse();
|
||||
message.active = object.active ?? "";
|
||||
message.providers = object.providers?.map((e) => ProviderInfo.fromPartial(e)) || [];
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
export type ModelsServiceService = typeof ModelsServiceService;
|
||||
export const ModelsServiceService = {
|
||||
listModels: {
|
||||
path: "/apf.ai.v1.ModelsService/ListModels" as const,
|
||||
requestStream: false as const,
|
||||
responseStream: false as const,
|
||||
requestSerialize: (value: ListModelsRequest): Buffer => Buffer.from(ListModelsRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): ListModelsRequest => ListModelsRequest.decode(value),
|
||||
responseSerialize: (value: ListModelsResponse): Buffer => Buffer.from(ListModelsResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): ListModelsResponse => ListModelsResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface ModelsServiceServer extends UntypedServiceImplementation {
|
||||
listModels: handleUnaryCall<ListModelsRequest, ListModelsResponse>;
|
||||
}
|
||||
|
||||
export interface ModelsServiceClient extends Client {
|
||||
listModels(
|
||||
request: ListModelsRequest,
|
||||
callback: (error: ServiceError | null, response: ListModelsResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
listModels(
|
||||
request: ListModelsRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: ListModelsResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
listModels(
|
||||
request: ListModelsRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: ListModelsResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
}
|
||||
|
||||
export const ModelsServiceClient = makeGenericClientConstructor(
|
||||
ModelsServiceService,
|
||||
"apf.ai.v1.ModelsService",
|
||||
) as unknown as {
|
||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): ModelsServiceClient;
|
||||
service: typeof ModelsServiceService;
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.8
|
||||
// protoc v3.19.1
|
||||
// source: rag.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
type CallOptions,
|
||||
type ChannelCredentials,
|
||||
Client,
|
||||
type ClientOptions,
|
||||
type ClientUnaryCall,
|
||||
type handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
type Metadata,
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import Long from "long";
|
||||
import { Principal } from "./common";
|
||||
import { Struct } from "./google/protobuf/struct";
|
||||
|
||||
export interface RagSearchFilters {
|
||||
source: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export interface RagSearchRequest {
|
||||
query: string;
|
||||
topK: number;
|
||||
filters?: RagSearchFilters | undefined;
|
||||
principal?: Principal | undefined;
|
||||
}
|
||||
|
||||
export interface Chunk {
|
||||
id: string;
|
||||
documentId: string;
|
||||
content: string;
|
||||
source: string;
|
||||
score: number;
|
||||
metadata?: { [key: string]: any } | undefined;
|
||||
}
|
||||
|
||||
export interface RagSearchResponse {
|
||||
chunks: Chunk[];
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
function createBaseRagSearchFilters(): RagSearchFilters {
|
||||
return { source: "", documentId: "" };
|
||||
}
|
||||
|
||||
export const RagSearchFilters: MessageFns<RagSearchFilters> = {
|
||||
encode(message: RagSearchFilters, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.source !== "") {
|
||||
writer.uint32(10).string(message.source);
|
||||
}
|
||||
if (message.documentId !== "") {
|
||||
writer.uint32(18).string(message.documentId);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): RagSearchFilters {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseRagSearchFilters();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.source = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.documentId = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): RagSearchFilters {
|
||||
return {
|
||||
source: isSet(object.source) ? globalThis.String(object.source) : "",
|
||||
documentId: isSet(object.documentId)
|
||||
? globalThis.String(object.documentId)
|
||||
: isSet(object.document_id)
|
||||
? globalThis.String(object.document_id)
|
||||
: "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: RagSearchFilters): unknown {
|
||||
const obj: any = {};
|
||||
if (message.source !== "") {
|
||||
obj.source = message.source;
|
||||
}
|
||||
if (message.documentId !== "") {
|
||||
obj.documentId = message.documentId;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<RagSearchFilters>, I>>(base?: I): RagSearchFilters {
|
||||
return RagSearchFilters.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<RagSearchFilters>, I>>(object: I): RagSearchFilters {
|
||||
const message = createBaseRagSearchFilters();
|
||||
message.source = object.source ?? "";
|
||||
message.documentId = object.documentId ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseRagSearchRequest(): RagSearchRequest {
|
||||
return { query: "", topK: 0, filters: undefined, principal: undefined };
|
||||
}
|
||||
|
||||
export const RagSearchRequest: MessageFns<RagSearchRequest> = {
|
||||
encode(message: RagSearchRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.query !== "") {
|
||||
writer.uint32(10).string(message.query);
|
||||
}
|
||||
if (message.topK !== 0) {
|
||||
writer.uint32(16).int32(message.topK);
|
||||
}
|
||||
if (message.filters !== undefined) {
|
||||
RagSearchFilters.encode(message.filters, writer.uint32(26).fork()).join();
|
||||
}
|
||||
if (message.principal !== undefined) {
|
||||
Principal.encode(message.principal, writer.uint32(34).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): RagSearchRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseRagSearchRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.query = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.topK = reader.int32();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.filters = RagSearchFilters.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.principal = Principal.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): RagSearchRequest {
|
||||
return {
|
||||
query: isSet(object.query) ? globalThis.String(object.query) : "",
|
||||
topK: isSet(object.topK)
|
||||
? globalThis.Number(object.topK)
|
||||
: isSet(object.top_k)
|
||||
? globalThis.Number(object.top_k)
|
||||
: 0,
|
||||
filters: isSet(object.filters) ? RagSearchFilters.fromJSON(object.filters) : undefined,
|
||||
principal: isSet(object.principal) ? Principal.fromJSON(object.principal) : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: RagSearchRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.query !== "") {
|
||||
obj.query = message.query;
|
||||
}
|
||||
if (message.topK !== 0) {
|
||||
obj.topK = Math.round(message.topK);
|
||||
}
|
||||
if (message.filters !== undefined) {
|
||||
obj.filters = RagSearchFilters.toJSON(message.filters);
|
||||
}
|
||||
if (message.principal !== undefined) {
|
||||
obj.principal = Principal.toJSON(message.principal);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<RagSearchRequest>, I>>(base?: I): RagSearchRequest {
|
||||
return RagSearchRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<RagSearchRequest>, I>>(object: I): RagSearchRequest {
|
||||
const message = createBaseRagSearchRequest();
|
||||
message.query = object.query ?? "";
|
||||
message.topK = object.topK ?? 0;
|
||||
message.filters = (object.filters !== undefined && object.filters !== null)
|
||||
? RagSearchFilters.fromPartial(object.filters)
|
||||
: undefined;
|
||||
message.principal = (object.principal !== undefined && object.principal !== null)
|
||||
? Principal.fromPartial(object.principal)
|
||||
: undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseChunk(): Chunk {
|
||||
return { id: "", documentId: "", content: "", source: "", score: 0, metadata: undefined };
|
||||
}
|
||||
|
||||
export const Chunk: MessageFns<Chunk> = {
|
||||
encode(message: Chunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.documentId !== "") {
|
||||
writer.uint32(18).string(message.documentId);
|
||||
}
|
||||
if (message.content !== "") {
|
||||
writer.uint32(26).string(message.content);
|
||||
}
|
||||
if (message.source !== "") {
|
||||
writer.uint32(34).string(message.source);
|
||||
}
|
||||
if (message.score !== 0) {
|
||||
writer.uint32(41).double(message.score);
|
||||
}
|
||||
if (message.metadata !== undefined) {
|
||||
Struct.encode(Struct.wrap(message.metadata), writer.uint32(50).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Chunk {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseChunk();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.documentId = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.content = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.source = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 41) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.score = reader.double();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 50) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.metadata = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Chunk {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
documentId: isSet(object.documentId)
|
||||
? globalThis.String(object.documentId)
|
||||
: isSet(object.document_id)
|
||||
? globalThis.String(object.document_id)
|
||||
: "",
|
||||
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||
source: isSet(object.source) ? globalThis.String(object.source) : "",
|
||||
score: isSet(object.score) ? globalThis.Number(object.score) : 0,
|
||||
metadata: isObject(object.metadata) ? object.metadata : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Chunk): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.documentId !== "") {
|
||||
obj.documentId = message.documentId;
|
||||
}
|
||||
if (message.content !== "") {
|
||||
obj.content = message.content;
|
||||
}
|
||||
if (message.source !== "") {
|
||||
obj.source = message.source;
|
||||
}
|
||||
if (message.score !== 0) {
|
||||
obj.score = message.score;
|
||||
}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = message.metadata;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Chunk>, I>>(base?: I): Chunk {
|
||||
return Chunk.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Chunk>, I>>(object: I): Chunk {
|
||||
const message = createBaseChunk();
|
||||
message.id = object.id ?? "";
|
||||
message.documentId = object.documentId ?? "";
|
||||
message.content = object.content ?? "";
|
||||
message.source = object.source ?? "";
|
||||
message.score = object.score ?? 0;
|
||||
message.metadata = object.metadata ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseRagSearchResponse(): RagSearchResponse {
|
||||
return { chunks: [], correlationId: "" };
|
||||
}
|
||||
|
||||
export const RagSearchResponse: MessageFns<RagSearchResponse> = {
|
||||
encode(message: RagSearchResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.chunks) {
|
||||
Chunk.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
if (message.correlationId !== "") {
|
||||
writer.uint32(18).string(message.correlationId);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): RagSearchResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseRagSearchResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.chunks.push(Chunk.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.correlationId = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): RagSearchResponse {
|
||||
return {
|
||||
chunks: globalThis.Array.isArray(object?.chunks) ? object.chunks.map((e: any) => Chunk.fromJSON(e)) : [],
|
||||
correlationId: isSet(object.correlationId)
|
||||
? globalThis.String(object.correlationId)
|
||||
: isSet(object.correlation_id)
|
||||
? globalThis.String(object.correlation_id)
|
||||
: "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: RagSearchResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.chunks?.length) {
|
||||
obj.chunks = message.chunks.map((e) => Chunk.toJSON(e));
|
||||
}
|
||||
if (message.correlationId !== "") {
|
||||
obj.correlationId = message.correlationId;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<RagSearchResponse>, I>>(base?: I): RagSearchResponse {
|
||||
return RagSearchResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<RagSearchResponse>, I>>(object: I): RagSearchResponse {
|
||||
const message = createBaseRagSearchResponse();
|
||||
message.chunks = object.chunks?.map((e) => Chunk.fromPartial(e)) || [];
|
||||
message.correlationId = object.correlationId ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Stateless retrieval. The server runs a single SQL query that combines
|
||||
* vector similarity with the principal's ACL clause — there is no
|
||||
* retrieval path that bypasses the ACL filter.
|
||||
*/
|
||||
export type RagServiceService = typeof RagServiceService;
|
||||
export const RagServiceService = {
|
||||
search: {
|
||||
path: "/apf.ai.v1.RagService/Search" as const,
|
||||
requestStream: false as const,
|
||||
responseStream: false as const,
|
||||
requestSerialize: (value: RagSearchRequest): Buffer => Buffer.from(RagSearchRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): RagSearchRequest => RagSearchRequest.decode(value),
|
||||
responseSerialize: (value: RagSearchResponse): Buffer => Buffer.from(RagSearchResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): RagSearchResponse => RagSearchResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface RagServiceServer extends UntypedServiceImplementation {
|
||||
search: handleUnaryCall<RagSearchRequest, RagSearchResponse>;
|
||||
}
|
||||
|
||||
export interface RagServiceClient extends Client {
|
||||
search(
|
||||
request: RagSearchRequest,
|
||||
callback: (error: ServiceError | null, response: RagSearchResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
search(
|
||||
request: RagSearchRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: RagSearchResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
search(
|
||||
request: RagSearchRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: RagSearchResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
}
|
||||
|
||||
export const RagServiceClient = makeGenericClientConstructor(RagServiceService, "apf.ai.v1.RagService") as unknown as {
|
||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): RagServiceClient;
|
||||
service: typeof RagServiceService;
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package apf.ai.v1;
|
||||
|
||||
import "common.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||
|
||||
// Streaming chat. Server returns a stream of ChatEvent; one event per
|
||||
// model delta, citation, agent step, tool-call request, error, or done.
|
||||
// Conversation state is NOT persisted — the caller sends the full
|
||||
// message history every call. `conversation_id` exists only for audit
|
||||
// correlation.
|
||||
service ChatService {
|
||||
rpc Chat(ChatRequest) returns (stream ChatEvent);
|
||||
}
|
||||
|
||||
message RagOptions {
|
||||
bool enabled = 1;
|
||||
int32 top_k = 2; // 0 = use server default
|
||||
}
|
||||
|
||||
message ChatRequest {
|
||||
repeated ChatMessage messages = 1;
|
||||
string conversation_id = 2;
|
||||
string model = 3;
|
||||
string provider = 4;
|
||||
repeated ToolDescriptor tools_available = 5;
|
||||
RagOptions rag = 6;
|
||||
// Optional for JWT auth (overridden by JWT claims). Required for
|
||||
// API-key auth — but API-key callers should use the HTTP surface.
|
||||
Principal principal = 7;
|
||||
}
|
||||
|
||||
message TokenEvent {
|
||||
string token = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message CitationEvent {
|
||||
string chunk_id = 1;
|
||||
string document_id = 2;
|
||||
string source = 3;
|
||||
double score = 4;
|
||||
string snippet = 5;
|
||||
}
|
||||
|
||||
message AgentStepEvent {
|
||||
string agent = 1;
|
||||
string step = 2;
|
||||
string step_id = 3;
|
||||
}
|
||||
|
||||
message ToolCallEvent {
|
||||
string call_id = 1;
|
||||
string name = 2;
|
||||
google.protobuf.Struct args = 3;
|
||||
}
|
||||
|
||||
message ErrorEvent {
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
bool retriable = 3;
|
||||
}
|
||||
|
||||
message DoneStats {
|
||||
int32 tokens_in = 1;
|
||||
int32 tokens_out = 2;
|
||||
int32 chunks_retrieved = 3;
|
||||
}
|
||||
|
||||
message DoneEvent {
|
||||
DoneStats stats = 1;
|
||||
}
|
||||
|
||||
// Polymorphic stream payload. Mirrors the SSE shapes from the OpenAPI
|
||||
// surface (kept side-by-side for third-party callers). gRPC's `oneof`
|
||||
// gives us a tagged union without the SSE JSON discriminator.
|
||||
message ChatEvent {
|
||||
oneof event {
|
||||
TokenEvent token = 1;
|
||||
CitationEvent citation = 2;
|
||||
AgentStepEvent agent_step = 3;
|
||||
ToolCallEvent tool_call = 4;
|
||||
ErrorEvent error = 5;
|
||||
DoneEvent done = 6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package apf.ai.v1;
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||
|
||||
// Identity-bearing principal shipped with every request (carried in the JWT for
|
||||
// internal callers, or in the request body for API-key callers — same shape).
|
||||
message Principal {
|
||||
string subject = 1;
|
||||
repeated string roles = 2;
|
||||
map<string, string> attributes = 3;
|
||||
}
|
||||
|
||||
// Default-deny ACL attached to every ingested chunk.
|
||||
// At least one of allowed_roles or allowed_subjects must be non-empty,
|
||||
// enforced at the controller, the DB CHECK constraint, and the post-filter.
|
||||
message AclSpec {
|
||||
repeated string allowed_roles = 1;
|
||||
repeated string allowed_subjects = 2;
|
||||
repeated string denied_roles = 3;
|
||||
map<string, string> required_attributes = 4;
|
||||
}
|
||||
|
||||
enum ChatRole {
|
||||
CHAT_ROLE_UNSPECIFIED = 0;
|
||||
CHAT_ROLE_SYSTEM = 1;
|
||||
CHAT_ROLE_USER = 2;
|
||||
CHAT_ROLE_ASSISTANT = 3;
|
||||
CHAT_ROLE_TOOL = 4;
|
||||
}
|
||||
|
||||
message ChatMessage {
|
||||
ChatRole role = 1;
|
||||
string content = 2;
|
||||
string tool_call_id = 3;
|
||||
string name = 4;
|
||||
}
|
||||
|
||||
// JSON-schema descriptor for caller-side tool execution. The AI service is
|
||||
// tool-blind: it emits ToolCall events; the caller executes and posts back.
|
||||
message ToolDescriptor {
|
||||
string name = 1;
|
||||
string description = 2;
|
||||
google.protobuf.Struct schema = 3;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package apf.ai.v1;
|
||||
|
||||
import "common.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||
|
||||
// Document ingestion. Every chunk must carry an explicit ACL: empty
|
||||
// ACL is default-deny and rejected with InvalidArgument (gRPC) /
|
||||
// 422 (HTTP).
|
||||
service IngestionService {
|
||||
rpc IngestDocument(IngestDocumentRequest) returns (IngestDocumentResponse);
|
||||
rpc GetDocument(GetDocumentRequest) returns (DocumentSummary);
|
||||
rpc DeleteDocument(DeleteDocumentRequest) returns (DeleteDocumentResponse);
|
||||
}
|
||||
|
||||
message IngestChunk {
|
||||
string content = 1;
|
||||
google.protobuf.Struct metadata = 2;
|
||||
AclSpec acl = 3;
|
||||
}
|
||||
|
||||
message IngestDocumentRequest {
|
||||
string source = 1;
|
||||
string external_id = 2;
|
||||
string title = 3;
|
||||
repeated IngestChunk chunks = 4;
|
||||
Principal principal = 5;
|
||||
}
|
||||
|
||||
message IngestDocumentResponse {
|
||||
string document_id = 1;
|
||||
int32 chunks_ingested = 2;
|
||||
}
|
||||
|
||||
message GetDocumentRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DocumentSummary {
|
||||
string id = 1;
|
||||
string source_uri = 2;
|
||||
string title = 3;
|
||||
string external_id = 4;
|
||||
google.protobuf.Timestamp ingested_at = 5;
|
||||
int32 chunk_count = 6;
|
||||
}
|
||||
|
||||
message DeleteDocumentRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DeleteDocumentResponse {
|
||||
bool deleted = 1;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package apf.ai.v1;
|
||||
|
||||
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||
|
||||
service ModelsService {
|
||||
rpc ListModels(ListModelsRequest) returns (ListModelsResponse);
|
||||
}
|
||||
|
||||
message ListModelsRequest {}
|
||||
|
||||
message ProviderInfo {
|
||||
string discriminator = 1;
|
||||
string capabilities = 2;
|
||||
string endpoint = 3;
|
||||
string model = 4;
|
||||
string embedding_model = 5;
|
||||
}
|
||||
|
||||
message ListModelsResponse {
|
||||
string active = 1;
|
||||
repeated ProviderInfo providers = 2;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package apf.ai.v1;
|
||||
|
||||
import "common.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||
|
||||
// Stateless retrieval. The server runs a single SQL query that combines
|
||||
// vector similarity with the principal's ACL clause — there is no
|
||||
// retrieval path that bypasses the ACL filter.
|
||||
service RagService {
|
||||
rpc Search(RagSearchRequest) returns (RagSearchResponse);
|
||||
}
|
||||
|
||||
message RagSearchFilters {
|
||||
string source = 1;
|
||||
string document_id = 2;
|
||||
}
|
||||
|
||||
message RagSearchRequest {
|
||||
string query = 1;
|
||||
int32 top_k = 2;
|
||||
RagSearchFilters filters = 3;
|
||||
Principal principal = 4;
|
||||
}
|
||||
|
||||
message Chunk {
|
||||
string id = 1;
|
||||
string document_id = 2;
|
||||
string content = 3;
|
||||
string source = 4;
|
||||
double score = 5;
|
||||
google.protobuf.Struct metadata = 6;
|
||||
}
|
||||
|
||||
message RagSearchResponse {
|
||||
repeated Chunk chunks = 1;
|
||||
string correlation_id = 2;
|
||||
}
|
||||
+9
-5
@@ -5,11 +5,15 @@ export default [
|
||||
...nx.configs['flat/typescript'],
|
||||
...nx.configs['flat/javascript'],
|
||||
{
|
||||
"ignores": [
|
||||
"**/dist",
|
||||
"**/out-tsc",
|
||||
"**/vitest.config.*.timestamp*"
|
||||
]
|
||||
"ignores": [
|
||||
"**/dist",
|
||||
"**/out-tsc",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
// Generated gRPC TypeScript stubs — hand-rules do not apply to
|
||||
// codegen output. Lint the wrapper modules in `ai-client/`, not
|
||||
// the ts-proto-emitted files under `gen/`.
|
||||
"apps/portal-bff/src/grpc/gen/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose",
|
||||
"ci:gzip-budgets": "node scripts/check-gzip-budgets.mjs",
|
||||
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm ci:gzip-budgets && pnpm exec lhci autorun --config=./lighthouserc.js",
|
||||
"grpc:codegen": "grpc_tools_node_protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=apps/portal-bff/src/grpc/gen/apf-ai --ts_proto_opt=outputServices=grpc-js,esModuleInterop=true,forceLong=long,useOptionals=messages,exportCommonSymbols=false --proto_path=apps/portal-bff/src/grpc/proto/apf-ai apps/portal-bff/src/grpc/proto/apf-ai/*.proto",
|
||||
"grpc:sync": "node scripts/sync-ai-protos.mjs",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
@@ -28,6 +30,7 @@
|
||||
"@prisma/engines",
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"grpc-tools",
|
||||
"less",
|
||||
"lmdb",
|
||||
"msgpackr-extract",
|
||||
@@ -108,6 +111,7 @@
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-playwright": "^1.6.2",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^30.0.2",
|
||||
"jest-environment-node": "^30.0.2",
|
||||
@@ -124,6 +128,7 @@
|
||||
"tailwindcss": "^4.2.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-proto": "^2.7.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
@@ -143,6 +148,8 @@
|
||||
"@angular/platform-browser": "~21.2.0",
|
||||
"@angular/router": "~21.2.0",
|
||||
"@azure/msal-node": "^5.2.1",
|
||||
"@bufbuild/protobuf": "^2.10.2",
|
||||
"@grpc/grpc-js": "^1.13.0",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.0",
|
||||
@@ -180,6 +187,7 @@
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"jose": "^6.2.3",
|
||||
"long": "^5.2.3",
|
||||
"lucide-angular": "^1.0.0",
|
||||
"nestjs-cls": "^6.2.0",
|
||||
"nestjs-pino": "^4.6.1",
|
||||
|
||||
Generated
+110
-6
@@ -49,6 +49,12 @@ importers:
|
||||
'@azure/msal-node':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
'@bufbuild/protobuf':
|
||||
specifier: ^2.10.2
|
||||
version: 2.12.0
|
||||
'@grpc/grpc-js':
|
||||
specifier: ^1.13.0
|
||||
version: 1.14.3
|
||||
'@nestjs/common':
|
||||
specifier: ^11.0.0
|
||||
version: 11.1.21(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@@ -160,6 +166,9 @@ importers:
|
||||
jose:
|
||||
specifier: ^6.2.3
|
||||
version: 6.2.3
|
||||
long:
|
||||
specifier: ^5.2.3
|
||||
version: 5.3.2
|
||||
lucide-angular:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@angular/common@21.2.13(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))
|
||||
@@ -347,6 +356,9 @@ importers:
|
||||
eslint-plugin-playwright:
|
||||
specifier: ^1.6.2
|
||||
version: 1.8.3(eslint@9.39.4(jiti@2.7.0))
|
||||
grpc-tools:
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.1(encoding@0.1.13)
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
@@ -395,6 +407,9 @@ importers:
|
||||
ts-node:
|
||||
specifier: 10.9.2
|
||||
version: 10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.21))(@types/node@24.12.4)(typescript@5.9.3)
|
||||
ts-proto:
|
||||
specifier: ^2.7.0
|
||||
version: 2.11.8
|
||||
tslib:
|
||||
specifier: ^2.3.0
|
||||
version: 2.8.1
|
||||
@@ -2541,6 +2556,11 @@ packages:
|
||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.3':
|
||||
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@mermaid-js/mermaid-mindmap@9.3.0':
|
||||
resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
|
||||
|
||||
@@ -5354,6 +5374,10 @@ packages:
|
||||
resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==}
|
||||
hasBin: true
|
||||
|
||||
abbrev@3.0.1:
|
||||
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
abbrev@4.0.0:
|
||||
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
@@ -5855,6 +5879,10 @@ packages:
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
|
||||
case-anything@2.1.13:
|
||||
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
|
||||
engines: {node: '>=12.13'}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@@ -6613,6 +6641,11 @@ packages:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6679,6 +6712,9 @@ packages:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dprint-node@1.0.8:
|
||||
resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7319,6 +7355,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
grpc-tools@1.13.1:
|
||||
resolution: {integrity: sha512-0sttMUxThNIkCTJq5qI0xXMz5zWqV2u3yG1kR3Sj9OokGIoyRBFjoInK9NyW7x5fH7knj48Roh1gq5xbl0VoDQ==}
|
||||
hasBin: true
|
||||
|
||||
hachure-fill@0.5.2:
|
||||
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
||||
|
||||
@@ -8715,6 +8755,11 @@ packages:
|
||||
non-layered-tidy-tree-layout@2.0.2:
|
||||
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
|
||||
|
||||
nopt@8.1.0:
|
||||
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
hasBin: true
|
||||
|
||||
nopt@9.0.0:
|
||||
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
@@ -10547,6 +10592,16 @@ packages:
|
||||
'@swc/wasm':
|
||||
optional: true
|
||||
|
||||
ts-poet@6.12.0:
|
||||
resolution: {integrity: sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==}
|
||||
|
||||
ts-proto-descriptors@2.1.0:
|
||||
resolution: {integrity: sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==}
|
||||
|
||||
ts-proto@2.11.8:
|
||||
resolution: {integrity: sha512-+5hzECnyVB33jxjG1BIdzAHcRBm7hjnm8womdJVp2A7xJWihP0drHHVsXYTr9i/LpWNGfh80I+AVVNzFM5AwJw==}
|
||||
hasBin: true
|
||||
|
||||
tsconfig-paths-webpack-plugin@4.2.0:
|
||||
resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -13702,6 +13757,19 @@ snapshots:
|
||||
|
||||
'@lukeed/csprng@1.1.0': {}
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)':
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
detect-libc: 2.1.2
|
||||
https-proxy-agent: 7.0.6
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
nopt: 8.1.0
|
||||
semver: 7.8.0
|
||||
tar: 7.5.15
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@mermaid-js/mermaid-mindmap@9.3.0':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 6.0.4
|
||||
@@ -14157,7 +14225,7 @@ snapshots:
|
||||
|
||||
'@npmcli/fs@5.0.0':
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
semver: 7.8.0
|
||||
|
||||
'@npmcli/git@7.0.2':
|
||||
dependencies:
|
||||
@@ -14167,7 +14235,7 @@ snapshots:
|
||||
lru-cache: 11.3.6
|
||||
npm-pick-manifest: 11.0.3
|
||||
proc-log: 6.1.0
|
||||
semver: 7.7.4
|
||||
semver: 7.8.0
|
||||
which: 6.0.1
|
||||
|
||||
'@npmcli/installed-package-contents@4.0.0':
|
||||
@@ -14184,7 +14252,7 @@ snapshots:
|
||||
hosted-git-info: 9.0.3
|
||||
json-parse-even-better-errors: 5.0.0
|
||||
proc-log: 6.1.0
|
||||
semver: 7.7.4
|
||||
semver: 7.8.0
|
||||
spdx-expression-parse: 4.0.0
|
||||
|
||||
'@npmcli/promise-spawn@9.0.1':
|
||||
@@ -16984,6 +17052,8 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
abbrev@3.0.1: {}
|
||||
|
||||
abbrev@4.0.0: {}
|
||||
|
||||
accepts@1.3.8:
|
||||
@@ -17564,6 +17634,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
|
||||
case-anything@2.1.13: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
@@ -18318,6 +18390,8 @@ snapshots:
|
||||
|
||||
destroy@1.2.0: {}
|
||||
|
||||
detect-libc@1.0.3: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
detect-newline@3.1.0: {}
|
||||
@@ -18376,6 +18450,10 @@ snapshots:
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dprint-node@1.0.8:
|
||||
dependencies:
|
||||
detect-libc: 1.0.3
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -19158,6 +19236,13 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
grpc-tools@1.13.1(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
hachure-fill@0.5.2: {}
|
||||
|
||||
handle-thing@2.0.1: {}
|
||||
@@ -20756,7 +20841,7 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
nopt: 9.0.0
|
||||
proc-log: 6.1.0
|
||||
semver: 7.7.4
|
||||
semver: 7.8.0
|
||||
tar: 7.5.15
|
||||
tinyglobby: 0.2.16
|
||||
undici: 6.25.0
|
||||
@@ -20775,6 +20860,10 @@ snapshots:
|
||||
non-layered-tidy-tree-layout@2.0.2:
|
||||
optional: true
|
||||
|
||||
nopt@8.1.0:
|
||||
dependencies:
|
||||
abbrev: 3.0.1
|
||||
|
||||
nopt@9.0.0:
|
||||
dependencies:
|
||||
abbrev: 4.0.0
|
||||
@@ -20787,7 +20876,7 @@ snapshots:
|
||||
|
||||
npm-install-checks@8.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
semver: 7.8.0
|
||||
|
||||
npm-normalize-package-bin@5.0.0: {}
|
||||
|
||||
@@ -20808,7 +20897,7 @@ snapshots:
|
||||
npm-install-checks: 8.0.0
|
||||
npm-normalize-package-bin: 5.0.0
|
||||
npm-package-arg: 13.0.2
|
||||
semver: 7.7.4
|
||||
semver: 7.8.0
|
||||
|
||||
npm-registry-fetch@19.1.1:
|
||||
dependencies:
|
||||
@@ -22881,6 +22970,21 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
|
||||
|
||||
ts-poet@6.12.0:
|
||||
dependencies:
|
||||
dprint-node: 1.0.8
|
||||
|
||||
ts-proto-descriptors@2.1.0:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.12.0
|
||||
|
||||
ts-proto@2.11.8:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.12.0
|
||||
case-anything: 2.1.13
|
||||
ts-poet: 6.12.0
|
||||
ts-proto-descriptors: 2.1.0
|
||||
|
||||
tsconfig-paths-webpack-plugin@4.2.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
// Refresh the vendored apf-ai-service protos from the sibling
|
||||
// working tree. Developer convenience only — not invoked from CI.
|
||||
//
|
||||
// Per ADR-0024 §"Sub-decision 3 — vendored protos", the BFF holds a
|
||||
// committed copy of the `.proto` files; this script keeps the copy
|
||||
// in sync with the upstream repo when a contract change lands. The
|
||||
// generated TypeScript stubs are NOT regenerated here — chain with
|
||||
// `pnpm run grpc:codegen` if needed:
|
||||
//
|
||||
// pnpm run grpc:sync && pnpm run grpc:codegen
|
||||
//
|
||||
// Exits non-zero (with a clear message) when the sibling repo is not
|
||||
// where the script expects, so a contributor whose layout differs
|
||||
// gets a useful pointer instead of a silent no-op.
|
||||
|
||||
import { existsSync, readdirSync, copyFileSync, statSync } from 'node:fs';
|
||||
import { join, resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const WORKSPACE_ROOT = resolve(HERE, '..');
|
||||
const UPSTREAM = resolve(WORKSPACE_ROOT, '..', 'apf-ai-service', 'contract', 'proto');
|
||||
const VENDORED = resolve(WORKSPACE_ROOT, 'apps', 'portal-bff', 'src', 'grpc', 'proto', 'apf-ai');
|
||||
|
||||
if (!existsSync(UPSTREAM) || !statSync(UPSTREAM).isDirectory()) {
|
||||
console.error(
|
||||
`apf-ai-service protos not found at ${UPSTREAM}.\n` +
|
||||
`This script expects the sibling working tree at ../apf-ai-service ` +
|
||||
`relative to this workspace. Adjust the path or clone the repo and retry.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = readdirSync(UPSTREAM).filter((f) => f.endsWith('.proto'));
|
||||
if (files.length === 0) {
|
||||
console.error(`No .proto files under ${UPSTREAM} — nothing to sync.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
copyFileSync(join(UPSTREAM, file), join(VENDORED, file));
|
||||
console.log(`synced ${file}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n${files.length} proto file(s) refreshed into ${VENDORED}.\n` +
|
||||
`Next: pnpm run grpc:codegen (regenerate TypeScript stubs).`,
|
||||
);
|
||||
Reference in New Issue
Block a user