Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
37 changes: 37 additions & 0 deletions .github/workflows/heap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Heap memory

on:
pull_request:
push:
branches:
- main

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
heap:
name: Heap regression check
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.x

- name: Install
run: npm ci

- name: Build
run: npm run build

- name: Run heap check
run: scripts/heap-check
6 changes: 4 additions & 2 deletions .jscpd.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
"threshold": 5,
"reporters": ["console"],
"reporters": [
"console"
],
"ignore": [
"NOTICE.txt",
"node_modules/**",
"dist/**",
"build/**",
"src/es/apis/schemas/**",
"src/es/apis/**",
"src/cloud/apis/**",
"packages/es-schemas/src/**",
"test/**",
"codegen/**",
"specs/**",
Expand Down
2 changes: 1 addition & 1 deletion .mega-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ TYPESCRIPT_ES_CONFIG_FILE: eslint.config.js
FILTER_REGEX_EXCLUDE: "(node_modules/|dist/|build/|\\.git/)"

# Skip generated code for ESLint (39k lines of schemas + 2k lines of cloud APIs)
TYPESCRIPT_ES_FILTER_REGEX_EXCLUDE: "(src/es/apis/schemas/|src/cloud/apis/|packages/es-schemas/src/)"
TYPESCRIPT_ES_FILTER_REGEX_EXCLUDE: "(src/es/apis/schemas/|src/cloud/apis/)"

# yamllint: skip test fixtures (custom multi-doc YAML DSL)
YAML_YAMLLINT_CONFIG_FILE: .yamllint.yml
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture Decisions

Read this before proposing structural changes. Each section documents a deliberate choice made for startup performance or memory efficiency, validated by `scripts/perf-check`. Do not refactor these unless benchmarks prove the change is neutral.
Read this before proposing structural changes. Each section documents a deliberate choice made for startup performance or memory efficiency, validated by `scripts/perf-check` (startup latency) and `scripts/heap-check` (heap allocation). Do not refactor these unless benchmarks prove the change is neutral.

## Factory Core Split (`factory-core.ts` / `factory.ts`)

Expand Down
354 changes: 350 additions & 4 deletions NOTICE.txt

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions SERVERLESS_SCHEMAS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Serverless API support in `@elastic/schemas`

`src/cloud/serverless-apis.ts` is currently hand-maintained. To migrate it to
`@elastic/schemas` (matching the pattern used for hosted cloud and ES/Kibana
APIs), the following additions are required in that package.

## New directory structure

```
src/serverless/
json/ # raw JSON schemas (follows existing pattern)

lib/serverless/
tools/
manifest.ts # exports serverlessManifest
manifest.d.ts
types.ts # re-exports CloudApiDefinition, HttpMethod
types.d.ts
index.ts
index.d.ts
apis/
elasticsearch-projects.ts # exports elasticsearchProjectsDefinitions
observability-projects.ts # exports observabilityProjectsDefinitions
security-projects.ts # exports securityProjectsDefinitions
regions.ts # exports regionsDefinitions
traffic-filters.ts # exports trafficFiltersDefinitions
linked-projects.ts # exports linkedProjectsDefinitions
linked-candidate-projects.ts # exports linkedCandidateProjectsDefinitions
```

## manifest.ts

Exports a single `serverlessManifest` constant of type `readonly ApiRegistryMeta[]`.
Each entry follows the same shape used by `esManifest` and `cloudManifest`:

```ts
import type { ApiRegistryMeta } from '../registry.ts'

export const serverlessManifest: readonly ApiRegistryMeta[] = [
{ id: 'elasticsearch-projects.list-elasticsearch-projects', name: 'list-elasticsearch-projects', namespace: 'elasticsearch-projects', description: 'Get Elasticsearch projects', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.create-elasticsearch-project', name: 'create-elasticsearch-project', namespace: 'elasticsearch-projects', description: 'Create an Elasticsearch project', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.get-elasticsearch-project', name: 'get-elasticsearch-project', namespace: 'elasticsearch-projects', description: 'Get an Elasticsearch project', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.delete-elasticsearch-project', name: 'delete-elasticsearch-project', namespace: 'elasticsearch-projects', description: 'Delete an Elasticsearch project', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.patch-elasticsearch-project', name: 'patch-elasticsearch-project', namespace: 'elasticsearch-projects', description: 'Update an Elasticsearch project', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.reset-elasticsearch-project-credentials', name: 'reset-elasticsearch-project-credentials', namespace: 'elasticsearch-projects', description: 'Reset the project credentials', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.resume-elasticsearch-project', name: 'resume-elasticsearch-project', namespace: 'elasticsearch-projects', description: 'Resume Elasticsearch project', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.get-elasticsearch-project-roles', name: 'get-elasticsearch-project-roles', namespace: 'elasticsearch-projects', description: 'Get roles for an Elasticsearch project', namespaceFile: 'elasticsearch-projects' },
{ id: 'elasticsearch-projects.get-elasticsearch-project-status', name: 'get-elasticsearch-project-status', namespace: 'elasticsearch-projects', description: 'Get the status of an Elasticsearch project', namespaceFile: 'elasticsearch-projects' },

{ id: 'observability-projects.list-observability-projects', name: 'list-observability-projects', namespace: 'observability-projects', description: 'Get Observability projects', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.create-observability-project', name: 'create-observability-project', namespace: 'observability-projects', description: 'Create an observability project', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.get-observability-project', name: 'get-observability-project', namespace: 'observability-projects', description: 'Get an Observability project', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.delete-observability-project', name: 'delete-observability-project', namespace: 'observability-projects', description: 'Delete an Observability project', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.patch-observability-project', name: 'patch-observability-project', namespace: 'observability-projects', description: 'Update an Observability project', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.reset-observability-project-credentials', name: 'reset-observability-project-credentials', namespace: 'observability-projects', description: 'Reset the project credentials', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.resume-observability-project', name: 'resume-observability-project', namespace: 'observability-projects', description: 'Resume Observability project', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.get-observability-project-roles', name: 'get-observability-project-roles', namespace: 'observability-projects', description: 'Get roles for an Observability project', namespaceFile: 'observability-projects' },
{ id: 'observability-projects.get-observability-project-status', name: 'get-observability-project-status', namespace: 'observability-projects', description: 'Get the status of an Observability project', namespaceFile: 'observability-projects' },

{ id: 'security-projects.list-security-projects', name: 'list-security-projects', namespace: 'security-projects', description: 'Get Security projects', namespaceFile: 'security-projects' },
{ id: 'security-projects.create-security-project', name: 'create-security-project', namespace: 'security-projects', description: 'Create a security project', namespaceFile: 'security-projects' },
{ id: 'security-projects.get-security-project', name: 'get-security-project', namespace: 'security-projects', description: 'Get a Security project', namespaceFile: 'security-projects' },
{ id: 'security-projects.delete-security-project', name: 'delete-security-project', namespace: 'security-projects', description: 'Delete a Security project', namespaceFile: 'security-projects' },
{ id: 'security-projects.patch-security-project', name: 'patch-security-project', namespace: 'security-projects', description: 'Update a Security project', namespaceFile: 'security-projects' },
{ id: 'security-projects.reset-security-project-credentials', name: 'reset-security-project-credentials', namespace: 'security-projects', description: 'Reset the project credentials', namespaceFile: 'security-projects' },
{ id: 'security-projects.resume-security-project', name: 'resume-security-project', namespace: 'security-projects', description: 'Resume Security project', namespaceFile: 'security-projects' },
{ id: 'security-projects.get-security-project-roles', name: 'get-security-project-roles', namespace: 'security-projects', description: 'Get roles for a Security project', namespaceFile: 'security-projects' },
{ id: 'security-projects.get-security-project-status', name: 'get-security-project-status', namespace: 'security-projects', description: 'Get the status of a Security project', namespaceFile: 'security-projects' },

{ id: 'regions.list-regions', name: 'list-regions', namespace: 'regions', description: 'Get regions', namespaceFile: 'regions' },
{ id: 'regions.get-region', name: 'get-region', namespace: 'regions', description: 'Get a region', namespaceFile: 'regions' },

{ id: 'traffic-filters.list-traffic-filters', name: 'list-traffic-filters', namespace: 'traffic-filters', description: 'List traffic filters', namespaceFile: 'traffic-filters' },
{ id: 'traffic-filters.create-traffic-filter', name: 'create-traffic-filter', namespace: 'traffic-filters', description: 'Create a traffic filter', namespaceFile: 'traffic-filters' },
{ id: 'traffic-filters.get-traffic-filter-metadata', name: 'get-traffic-filter-metadata', namespace: 'traffic-filters', description: 'List PrivateLink region metadata', namespaceFile: 'traffic-filters' },
{ id: 'traffic-filters.get-traffic-filter', name: 'get-traffic-filter', namespace: 'traffic-filters', description: 'Retrieves the traffic filter by ID.', namespaceFile: 'traffic-filters' },
{ id: 'traffic-filters.delete-traffic-filter', name: 'delete-traffic-filter', namespace: 'traffic-filters', description: 'Delete a traffic filter', namespaceFile: 'traffic-filters' },
{ id: 'traffic-filters.patch-traffic-filter', name: 'patch-traffic-filter', namespace: 'traffic-filters', description: 'Updates a traffic filter', namespaceFile: 'traffic-filters' },

{ id: 'linked-projects.get-elasticsearch-project-can-delete', name: 'get-elasticsearch-project-can-delete', namespace: 'linked-projects', description: 'Get Elasticsearch project delete status', namespaceFile: 'linked-projects' },
{ id: 'linked-projects.get-observability-project-can-delete', name: 'get-observability-project-can-delete', namespace: 'linked-projects', description: 'Get Observability project delete status', namespaceFile: 'linked-projects' },
{ id: 'linked-projects.get-security-project-can-delete', name: 'get-security-project-can-delete', namespace: 'linked-projects', description: 'Get Security project delete status', namespaceFile: 'linked-projects' },

{ id: 'linked-candidate-projects.get-elasticsearch-project-link-candidates', name: 'get-elasticsearch-project-link-candidates', namespace: 'linked-candidate-projects', description: 'Get Elasticsearch project link candidates', namespaceFile: 'linked-candidate-projects' },
{ id: 'linked-candidate-projects.get-observability-project-link-candidates', name: 'get-observability-project-link-candidates', namespace: 'linked-candidate-projects', description: 'Get Observability project link candidates', namespaceFile: 'linked-candidate-projects' },
{ id: 'linked-candidate-projects.get-security-project-link-candidates', name: 'get-security-project-link-candidates', namespace: 'linked-candidate-projects', description: 'Get Security project link candidates', namespaceFile: 'linked-candidate-projects' },
]
```

## Per-namespace api files

Each file exports a single `*Definitions` constant of type `CloudApiDefinition[]`
(imported from `'../../cloud/tools/types.js'`). The definition shape is identical
to the hosted cloud APIs: `name`, `namespace`, `description`, `method`, `path`,
`destructive`, and an optional `input` JSON Schema object with `x-found-in`
routing annotations (`'path'`, `'query'`, or `'body'`) on each property.

Example — `apis/regions.ts`:

```ts
import type { CloudApiDefinition } from '../../cloud/tools/types.js'

export const regionsDefinitions: CloudApiDefinition[] = [
{
name: 'list-regions',
namespace: 'regions',
description: 'Get regions',
method: 'GET',
path: '/api/v1/serverless/regions',
destructive: false,
},
{
name: 'get-region',
namespace: 'regions',
description: 'Get a region',
method: 'GET',
path: '/api/v1/serverless/regions/{id}',
destructive: false,
input: {
type: 'object',
properties: {
id: { type: 'string', description: 'ID of the region', 'x-found-in': 'path' },
},
required: ['id'],
},
},
]
```

The full set of definitions for all seven namespaces is documented in
`src/cloud/serverless-apis.ts`, which serves as the reference implementation
until this package support lands.

## `package.json` export additions

```json
"./serverless/tools/*.js": {
"types": "./lib/serverless/tools/*.d.ts",
"import": "./lib/serverless/tools/*.js",
"require": "./lib/cjs/serverless/tools/*.js"
},
"./serverless/json/*.json": "./src/serverless/json/*.json"
```

## `lib/index.ts` addition

```ts
export * as ServerlessTools from './serverless/tools/index.js'
```

## elastic-cli migration

Once the above lands in a published version of `@elastic/schemas`, the
migration in `elastic-cli` is:

1. Delete `src/cloud/serverless-apis.ts`.
2. Add `loadServerlessApis()` to `src/cloud/apis.ts` (or a new
`src/cloud/serverless-apis.ts` lazy loader) importing from
`@elastic/schemas/serverless/tools/manifest.js` and
`@elastic/schemas/serverless/tools/apis/*.js`, following the same pattern
as `loadCloudApis()`.
3. Update `src/cloud/register.ts` to call `loadServerlessApis()` alongside
`loadCloudApis()`.
2 changes: 1 addition & 1 deletion codegen/functional/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from './types.ts'
import { buildActionMap, mapAction } from './mapper.ts'
import type { MappedAction } from './mapper.ts'
import type { SchemaArgDefinition } from '../../src/lib/schema-args.ts'
import type { SchemaArgDefinition } from '../../src/lib/json-schema-args.ts'

export interface GenerateResult {
/** bash script content */
Expand Down
35 changes: 4 additions & 31 deletions codegen/functional/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,23 @@
*/

import type { EsApiDefinition } from '../../src/es/types.ts'
import { resolveInput } from '../../src/es/types.ts'
import { extractSchemaArgs } from '../../src/lib/schema-args.ts'
import type { SchemaArgDefinition } from '../../src/lib/schema-args.ts'
import { extractSchemaArgs } from '../../src/lib/json-schema-args.ts'
import type { SchemaArgDefinition } from '../../src/lib/json-schema-args.ts'

/**
* Result of mapping a YAML dot-notation action to a CLI command.
* Contains the CLI arguments needed to invoke the command.
*/
export interface MappedAction {
/** CLI args: ['es', namespace?, name, ...flags] */
cliArgs: string[]
/** true if the action accepts a request body */
hasBody: boolean
/** schema args lookup by key, for resolving body field flags */
/** schema args lookup by key */
bodyArgsByKey: Map<string, SchemaArgDefinition>
/** set of body field keys */
bodyFields: Set<string>
}

/**
* Builds a lookup from YAML dot-notation action names to EsApiDefinitions.
*
* YAML uses `namespace.name` (e.g. "indices.create") or just `name` (e.g. "get").
* Definitions with `namespace` are keyed as `namespace.name`.
* Definitions without `namespace` are keyed as just `name`.
*/
export function buildActionMap (definitions: EsApiDefinition[]): Map<string, EsApiDefinition> {
const map = new Map<string, EsApiDefinition>()
for (const def of definitions) {
Expand All @@ -39,22 +30,11 @@ export function buildActionMap (definitions: EsApiDefinition[]): Map<string, EsA
return map
}

/**
* Maps a YAML test action to CLI arguments.
*
* @param action - dot-notation action name (e.g. "indices.create", "get")
* @param params - YAML action parameters (path + query params, excluding body)
* @param actionMap - lookup map from buildActionMap
* @returns MappedAction with CLI args, or null if the action isn't registered
*/
export function mapAction (
action: string,
params: Record<string, unknown>,
actionMap: Map<string, EsApiDefinition>
): MappedAction | null {
// YAML tests use underscore notation (e.g. "clear_scroll", "cat.ml_data_frame_analytics")
// but CLI definitions use kebab-case (e.g. "clear-scroll", "cat.ml-data-frame-analytics").
// Normalize by converting underscores to hyphens within each dot-separated segment.
const normalizedAction = action.split('.').map((s) => s.replace(/_/g, '-')).join('.')
const def = actionMap.get(action) ?? actionMap.get(normalizedAction)
if (def == null) return null
Expand All @@ -63,9 +43,7 @@ export function mapAction (
if (def.namespace != null) args.push(def.namespace)
args.push(def.name)

const schemaArgs = def.input != null
? extractSchemaArgs(resolveInput(def.input))
: []
const schemaArgs = def.input != null ? extractSchemaArgs(def.input) : []

const bodyFields = new Set(
schemaArgs.filter((a) => a.foundIn === 'body').map((a) => a.schemaKey)
Expand All @@ -78,13 +56,8 @@ export function mapAction (

for (const [key, value] of Object.entries(params)) {
if (key === 'ignore') continue

const argDef = argsByKey.get(key)
// Skip params the CLI doesn't expose as flags (e.g. cat's 'format')
if (argDef == null) continue

// Body fields from YAML params are passed as CLI flags (same as non-body params);
// they will be handled alongside any explicit body in buildCommand.
args.push(`--${argDef.cliFlag}`, String(value))
}

Expand Down
Loading
Loading