@xemahq/api-client-generator 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,136 +0,0 @@
1
- /**
2
- * Layer 1 of the DI-stub strategy: seed deterministic dummy env vars BEFORE
3
- * the service's `AppModule` is imported. NestJS modules read `process.env` at
4
- * import time, so this MUST run first.
5
- *
6
- * Every value is a throwaway placeholder. `app.init()` is deliberately skipped
7
- * during extraction, so nothing here is ever dialed — the values only need to
8
- * exist so import-time `getOrThrow`/`new URL(...)` calls don't crash.
9
- *
10
- * This is the union of the seeds the 41 hand-written `extract-openapi.ts`
11
- * scripts set. A service that reads an env var not covered here adds it via the
12
- * optional per-service `openapi.overrides.ts` `env` field (Layer 3) — the
13
- * generator never silently invents a value it wasn't told about beyond this
14
- * shared baseline.
15
- */
16
-
17
- const DUMMY_ENCRYPTION_KEY_HEX =
18
- '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
19
- const DUMMY_BASE64_KEY = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
20
-
21
- /** Set a default only if the var is unset (`??=` semantics). */
22
- function seed(vars: Record<string, string>): void {
23
- for (const [key, value] of Object.entries(vars)) {
24
- if (process.env[key] === undefined) {
25
- process.env[key] = value;
26
- }
27
- }
28
- }
29
-
30
- /** The throwaway database every extraction points at. Never dialed. */
31
- const SCRATCH_DB_NAME = 'dummy_openapi';
32
-
33
- /**
34
- * Per-service scratch Postgres schema.
35
- *
36
- * Extraction is run in PARALLEL by turbo across ~40 services. If they all point
37
- * at one database AND one `public` schema they share a single
38
- * `_prisma_migrations` table, so any code path that migrates at DI-construction
39
- * time interleaves foreign migration histories and wedges its neighbours. Giving
40
- * each service its own schema makes the scratch surfaces disjoint by
41
- * construction. Postgres identifiers are lower-cased and capped at 63 bytes.
42
- */
43
- export function openApiScratchSchema(serviceId: string): string {
44
- return `extract_${serviceId.replace(/[^a-zA-Z0-9]+/g, '_')}`
45
- .toLowerCase()
46
- .slice(0, 63);
47
- }
48
-
49
- /**
50
- * @param serviceId the `xema.api.serviceId` of the service being extracted —
51
- * used to derive a scratch schema unique to this extraction process.
52
- */
53
- export function seedOpenApiEnv(serviceId: string): void {
54
- const schema = openApiScratchSchema(serviceId);
55
- seed({
56
- NODE_ENV: 'development',
57
-
58
- // Extraction-mode flag. `XemaServiceModule` eagerly resolves peer URLs from
59
- // the KernelState/etcd registry at DI-construction time; with no live cluster
60
- // that throws and the module can't even be constructed for scanning. Setting
61
- // this makes the SDK's construction-time resolvers (`resolveHttpUrlEagerly`)
62
- // short-circuit to a non-routable `.invalid` stub. The var NAME is the
63
- // service-registry-nest `OPENAPI_EXTRACTION_ENV_VAR` contract; the SDK is a
64
- // higher layer, so we set the env by name rather than import it here.
65
- XEMA_OPENAPI_EXTRACTION: '1',
66
-
67
- // Database (Prisma + raw pools). The scratch schema is per-service — see
68
- // `openApiScratchSchema`. `DB_SCHEMA` is NOT redundant with the URL's
69
- // `?schema=`: `createBiomePrismaAdapterFromEnv` passes it as the adapter's
70
- // second-arg `{ schema }`, while `?schema=` is what a Prisma CLI/migrate
71
- // invocation reads.
72
- DATABASE_URL: `postgresql://dummy:dummy@localhost:5432/${SCRATCH_DB_NAME}?schema=${schema}`,
73
- DB_HOST: 'localhost',
74
- DB_PORT: '5432',
75
- DB_NAME: SCRATCH_DB_NAME,
76
- DB_SCHEMA: schema,
77
- DB_USER: 'dummy',
78
- DB_PASSWORD: 'dummy',
79
- REDIS_URL: 'redis://localhost:6379',
80
- OPENSEARCH_URL: 'http://localhost:9200',
81
-
82
- // Identity / auth.
83
- IDENTITY_API_URL: 'http://localhost:3053',
84
- IDENTITY_API_INTERNAL_TOKEN: 'dummy-token',
85
- JWT_PUBLIC_KEY_URL:
86
- 'http://localhost:8180/realms/dummy/protocol/openid-connect/certs',
87
- JWT_ISSUER: 'http://localhost:8180/realms/dummy',
88
- GATEWAY_JWT_SECRET: 'dummy-gateway-jwt-secret-for-openapi-extraction',
89
- OPENCODE_WORKER_PASSWORD: 'dummy-worker-password',
90
-
91
- // Service registry (etcd-only autodiscovery). `createKernelState` fail-fasts
92
- // without this; the etcd3 client is lazy (no dial until first call) and
93
- // extraction skips `app.init()`, so the dummy endpoint is never contacted.
94
- XEMA_ETCD_ENDPOINTS: 'localhost:2379',
95
-
96
- // Event hub. `WEBHOOK_URL` is the delivery callback every service that
97
- // SUBSCRIBES declares to `EventHubModule.forRootAsync` — it is boot-REQUIRED
98
- // (`getOrThrow`) in ~20 services, so it belongs in the shared baseline
99
- // rather than being re-declared in ~20 `openapi.overrides.ts` files.
100
- EVENT_HUB_URL: 'http://localhost:3128',
101
- WEBHOOK_URL: 'http://localhost:3000/webhooks/events',
102
-
103
- // Cross-service HTTP URLs (resolved via the registry in prod; dummy here).
104
- BACKLOG_API_URL: 'http://localhost:3100',
105
- MEMORY_API_URL: 'http://localhost:3060',
106
- LLM_REGISTRY_API_URL: 'http://localhost:3112',
107
- LLM_GATEWAY_API_URL: 'http://localhost:3110',
108
- ARTIFACT_STORE_API_URL: 'http://localhost:3130',
109
- KNOWLEDGE_BASE_API_URL: 'http://localhost:3200',
110
- PROJECT_REGISTRY_API_URL: 'http://localhost:3300',
111
- CONNECTOR_GATEWAY_API_URL: 'http://localhost:3900',
112
- DESIGN_SYSTEM_BUILDER_API_URL: 'http://localhost:3210',
113
- SEARCH_API_URL: 'http://localhost:3220',
114
- BILLING_API_URL: 'http://localhost:3140',
115
- CAPABILITY_REGISTRY_API_URL: 'http://localhost:3700',
116
-
117
- // SCM / git.
118
- SCM_WEBHOOK_SECRET: 'dummy-webhook-secret',
119
- SCM_WEBHOOK_BASE_URL: 'http://localhost:3500/webhooks',
120
- GITEA_URL: 'http://localhost:3000',
121
- GITEA_ADMIN_USER: 'gitea_admin',
122
- GITEA_ADMIN_PASS: 'gitea_admin_pass',
123
-
124
- // Encryption keys (hex 32-byte + base64 variants used across services).
125
- MASTER_ENCRYPTION_KEY: DUMMY_ENCRYPTION_KEY_HEX,
126
- ENCRYPTION_MASTER_KEY: DUMMY_BASE64_KEY,
127
- LLM_REGISTRY_ENCRYPTION_KEY:
128
- 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
129
- VARIABLE_ENCRYPTION_KEY:
130
- 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210',
131
-
132
- // Platform observability endpoints.
133
- PLATFORM_METRICS_ENDPOINT: 'http://localhost:9090/metrics',
134
- PLATFORM_AUDIT_ENDPOINT: 'http://localhost:3601/audit',
135
- });
136
- }
@@ -1,170 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
3
-
4
- import { ApiSurface } from '@xemahq/xema-decorators';
5
-
6
- /**
7
- * The per-service residue of the central generator: a `xema.api` block in the
8
- * service's own `package.json`. Everything else (spec paths, client dirs,
9
- * client package names, mutator path, orval mode) is derived from this plus
10
- * the repo layout — there is no `orval.config.ts`, no extract script.
11
- *
12
- * ```jsonc
13
- * // <service>/package.json
14
- * { "name": "backlog-api", "version": "0.1.0",
15
- * "xema": { "api": { "serviceId": "backlog-api", "biomeId": "backlog",
16
- * "title": "Backlog API", "description": "…" } } }
17
- * ```
18
- */
19
- export interface XemaApiConfig {
20
- readonly serviceId: string;
21
- readonly biomeId: string;
22
- readonly title: string;
23
- readonly description?: string;
24
- }
25
-
26
- export interface ResolvedServiceContext {
27
- /** Absolute path to the service directory (process cwd). */
28
- readonly serviceDir: string;
29
- /** Absolute path to the repository root (holds pnpm-workspace.yaml). */
30
- readonly repoRoot: string;
31
- /** The declared `xema.api` block. */
32
- readonly api: XemaApiConfig;
33
- /** The service `package.json` version — stamped into every surface client. */
34
- readonly version: string;
35
- }
36
-
37
- /** Walk up from `start` to the directory holding `pnpm-workspace.yaml`. */
38
- export function findRepoRoot(start: string): string {
39
- let dir = resolve(start);
40
- while (dir !== dirname(dir)) {
41
- if (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
42
- return dir;
43
- }
44
- dir = dirname(dir);
45
- }
46
- throw new Error(
47
- `api-client-generator: could not locate a repo root (pnpm-workspace.yaml) ` +
48
- `walking up from ${start}.`,
49
- );
50
- }
51
-
52
- /**
53
- * Read and validate the service's `package.json#xema.api` block + version.
54
- * Fail-fast on a missing/malformed block — the generator never guesses a
55
- * serviceId or biomeId.
56
- */
57
- export function resolveServiceContext(serviceDir: string): ResolvedServiceContext {
58
- const pkgPath = join(serviceDir, 'package.json');
59
- if (!existsSync(pkgPath)) {
60
- throw new Error(
61
- `api-client-generator: no package.json found at ${pkgPath}. Run the ` +
62
- `generator from a service directory.`,
63
- );
64
- }
65
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
66
- version?: unknown;
67
- xema?: { api?: Partial<XemaApiConfig> } | null;
68
- };
69
- const api = pkg.xema?.api;
70
- if (!api || typeof api !== 'object') {
71
- throw new Error(
72
- `api-client-generator: ${pkgPath} is missing the required \`xema.api\` ` +
73
- `block. Add: { "xema": { "api": { "serviceId": "<id>", "biomeId": ` +
74
- `"<biome>", "title": "<title>" } } }.`,
75
- );
76
- }
77
- const { serviceId, biomeId, title, description } = api;
78
- for (const [field, value] of Object.entries({ serviceId, biomeId, title })) {
79
- if (typeof value !== 'string' || value.trim() === '') {
80
- throw new Error(
81
- `api-client-generator: \`xema.api.${field}\` is required and must be a ` +
82
- `non-empty string in ${pkgPath}.`,
83
- );
84
- }
85
- }
86
- if (typeof pkg.version !== 'string' || pkg.version.trim() === '') {
87
- throw new Error(
88
- `api-client-generator: ${pkgPath} is missing a \`version\` — every ` +
89
- `generated surface client shares the service version.`,
90
- );
91
- }
92
- return {
93
- serviceDir: resolve(serviceDir),
94
- repoRoot: findRepoRoot(serviceDir),
95
- api: {
96
- serviceId: serviceId as string,
97
- biomeId: biomeId as string,
98
- title: title as string,
99
- ...(typeof description === 'string' ? { description } : {}),
100
- },
101
- version: pkg.version,
102
- };
103
- }
104
-
105
- /**
106
- * Short package name (without the `@xemahq/` scope) for a surface client.
107
- *
108
- * - public: `<serviceId>-client` (unchanged from today, e.g.
109
- * `backlog-api` → `backlog-api-client`)
110
- * - internal: surface segment inserted before a trailing `-api`, matching the
111
- * agreed shape `llm-registry-api` → `llm-registry-internal-api-client`;
112
- * services without a `-api` suffix fall back to
113
- * `<serviceId>-internal-client`.
114
- * - operator: same rule with `operator`.
115
- *
116
- * Deterministic for every serviceId — no name guessing, pure string rule.
117
- */
118
- export function surfaceClientShortName(
119
- serviceId: string,
120
- surface: ApiSurface,
121
- ): string {
122
- if (surface === ApiSurface.Public) {
123
- return `${serviceId}-client`;
124
- }
125
- const segment = surface; // 'internal' | 'operator'
126
- if (serviceId.endsWith('-api')) {
127
- const base = serviceId.slice(0, -'-api'.length);
128
- return `${base}-${segment}-api-client`;
129
- }
130
- return `${serviceId}-${segment}-client`;
131
- }
132
-
133
- /** Scoped npm package name for a surface client. */
134
- export function surfaceClientPackageName(
135
- serviceId: string,
136
- surface: ApiSurface,
137
- ): string {
138
- return `@xemahq/${surfaceClientShortName(serviceId, surface)}`;
139
- }
140
-
141
- /** Absolute client package directory under `<repoRoot>/packages/clients/`. */
142
- export function surfaceClientDir(
143
- ctx: ResolvedServiceContext,
144
- surface: ApiSurface,
145
- ): string {
146
- const dirName = surfaceClientShortName(ctx.api.serviceId, surface).replace(
147
- /-client$/,
148
- '',
149
- );
150
- return join(ctx.repoRoot, 'packages', 'clients', dirName);
151
- }
152
-
153
- /** Absolute path of a surface's OpenAPI document at the service root. */
154
- export function surfaceSpecPath(
155
- ctx: ResolvedServiceContext,
156
- surface: ApiSurface,
157
- /**
158
- * Where to write instead of the service directory.
159
- *
160
- * Exists so a drift GATE can extract without touching the working tree. Its
161
- * sibling `check-client-spec-drift.mjs` states the requirement plainly: a
162
- * check that regenerates in place and reads `git status` cannot be run by an
163
- * author mid-change without destroying their work, so nobody runs it. The
164
- * same has to be true of code→spec drift, and that is only possible if the
165
- * extractor can be pointed elsewhere.
166
- */
167
- outDir?: string,
168
- ): string {
169
- return join(outDir ?? ctx.serviceDir, `openapi.${surface}.json`);
170
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * Resolve the NestJS runtime classes from the SERVICE's own `node_modules`,
3
- * not the generator's.
4
- *
5
- * The generator is symlinked into each service as a workspace package; a static
6
- * `import … from '@nestjs/core'` would resolve to the generator's own copy of
7
- * Nest, producing a dual-package hazard — the `DiscoveryService` token built
8
- * into the app (service's Nest) would differ from the token we pass to
9
- * `app.get(...)` (generator's Nest), and DI resolution fails. Resolving these
10
- * from `serviceDir` guarantees one Nest instance end-to-end.
11
- *
12
- * (The decorator-metadata scan in `@xemahq/xema-decorators` is NOT subject to
13
- * this: it reads via the global `Reflect` polyfill and compares string keys,
14
- * so the generator's copy reads metadata stamped by the service's copy fine.)
15
- */
16
-
17
- import type { INestApplication, Type } from '@nestjs/common';
18
- import type {
19
- DiscoveryService,
20
- MetadataScanner,
21
- ModulesContainer,
22
- } from '@nestjs/core';
23
- import type { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
24
- import type { Test } from '@nestjs/testing';
25
-
26
- export interface ServiceNest {
27
- readonly Test: typeof Test;
28
- readonly DocumentBuilder: typeof DocumentBuilder;
29
- readonly SwaggerModule: typeof SwaggerModule;
30
- readonly DiscoveryService: Type<DiscoveryService>;
31
- readonly MetadataScanner: Type<MetadataScanner>;
32
- readonly ModulesContainer: Type<ModulesContainer>;
33
- }
34
-
35
- function requireFrom(
36
- serviceDir: string,
37
- request: string,
38
- ): Record<string, unknown> {
39
- const resolved = require.resolve(request, { paths: [serviceDir] });
40
-
41
- return require(resolved) as Record<string, unknown>;
42
- }
43
-
44
- export function loadServiceNest(serviceDir: string): ServiceNest {
45
- const core = requireFrom(serviceDir, '@nestjs/core');
46
- const testing = requireFrom(serviceDir, '@nestjs/testing');
47
- const swagger = requireFrom(serviceDir, '@nestjs/swagger');
48
- return {
49
- Test: testing.Test as typeof Test,
50
- DocumentBuilder: swagger.DocumentBuilder as typeof DocumentBuilder,
51
- SwaggerModule: swagger.SwaggerModule as typeof SwaggerModule,
52
- DiscoveryService: core.DiscoveryService as Type<DiscoveryService>,
53
- MetadataScanner: core.MetadataScanner as Type<MetadataScanner>,
54
- ModulesContainer: core.ModulesContainer as Type<ModulesContainer>,
55
- };
56
- }
57
-
58
- export type { INestApplication };