@forestadmin/agent-bff 1.15.1 → 1.17.0
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.
- package/README.md +15 -1
- package/dist/cli-core.d.ts +3 -0
- package/dist/cli-core.js +69 -10
- package/dist/cli-dispatch.d.ts +8 -2
- package/dist/cli-dispatch.js +90 -9
- package/dist/data/agent-query.d.ts +2 -0
- package/dist/data/agent-query.js +28 -17
- package/dist/data/data-routes-middleware.js +66 -26
- package/dist/data/pack-id.d.ts +1 -0
- package/dist/data/pack-id.js +4 -3
- package/dist/http/agent-error-mapper.js +15 -7
- package/dist/http/bff-http-error.d.ts +6 -1
- package/dist/http/bff-http-error.js +5 -4
- package/dist/http/bff-local-errors.d.ts +2 -0
- package/dist/http/bff-local-errors.js +12 -4
- package/dist/openapi/collect-unfolding.d.ts +23 -0
- package/dist/openapi/collect-unfolding.js +144 -0
- package/dist/openapi/component-pool.d.ts +18 -0
- package/dist/openapi/component-pool.js +28 -0
- package/dist/openapi/field-schemas.d.ts +9 -0
- package/dist/openapi/field-schemas.js +43 -0
- package/dist/openapi/names.d.ts +16 -0
- package/dist/openapi/names.js +28 -0
- package/dist/openapi/openapi-document.d.ts +2 -1
- package/dist/openapi/openapi-document.js +112 -66
- package/dist/openapi/openapi-routes.d.ts +4 -1
- package/dist/openapi/openapi-routes.js +46 -9
- package/dist/openapi/schemas.d.ts +14 -1
- package/dist/openapi/schemas.js +11 -10
- package/dist/openapi/unfolded-document.d.ts +28 -0
- package/dist/openapi/unfolded-document.js +52 -0
- package/dist/openapi/unfolded-paths.d.ts +20 -0
- package/dist/openapi/unfolded-paths.js +408 -0
- package/dist/openapi/unfolding.d.ts +66 -0
- package/dist/openapi/unfolding.js +17 -0
- package/dist/permissions/build-permission-hints.d.ts +40 -0
- package/dist/permissions/build-permission-hints.js +73 -0
- package/dist/permissions/can-role-perform.d.ts +12 -0
- package/dist/permissions/can-role-perform.js +18 -0
- package/dist/permissions/permissions-cache.d.ts +24 -0
- package/dist/permissions/permissions-cache.js +51 -0
- package/dist/permissions/permissions-client.d.ts +21 -0
- package/dist/permissions/permissions-client.js +27 -0
- package/dist/permissions/permissions-routes-middleware.d.ts +13 -0
- package/dist/permissions/permissions-routes-middleware.js +99 -0
- package/dist/read-model/agent-capabilities-fetcher.d.ts +8 -3
- package/dist/read-model/agent-capabilities-fetcher.js +15 -7
- package/dist/read-model/capabilities-cache.d.ts +11 -1
- package/dist/read-model/capabilities-cache.js +1 -1
- package/dist/read-model/read-model.d.ts +15 -0
- package/dist/read-model/read-model.js +39 -2
- package/dist/validation/capabilities-validator.d.ts +9 -0
- package/dist/validation/capabilities-validator.js +3 -1
- package/dist/validation/operator-normalizer.d.ts +6 -0
- package/dist/validation/operator-normalizer.js +11 -1
- package/dist/validation/validation-errors.js +7 -5
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const SCHEMA_PREFIX = '#/components/schemas/';
|
|
4
|
+
/**
|
|
5
|
+
* Registers schema components on demand. Nothing is registered eagerly: a component no path
|
|
6
|
+
* references trips redocly's unused-component rule, and which shared pieces an unfolded document
|
|
7
|
+
* needs depends on the schema it was built from.
|
|
8
|
+
*/
|
|
9
|
+
class ComponentPool {
|
|
10
|
+
constructor(registry) {
|
|
11
|
+
this.shared = new Set();
|
|
12
|
+
this.registry = registry;
|
|
13
|
+
}
|
|
14
|
+
/** A hand-written OpenAPI component. The caller owns name uniqueness. */
|
|
15
|
+
add(name, component) {
|
|
16
|
+
return this.registry.registerComponent('schemas', name, component).ref;
|
|
17
|
+
}
|
|
18
|
+
/** A zod schema shared by several paths, registered the first time it is referenced. */
|
|
19
|
+
reuse(name, schema) {
|
|
20
|
+
if (!this.shared.has(name)) {
|
|
21
|
+
this.registry.register(name, schema);
|
|
22
|
+
this.shared.add(name);
|
|
23
|
+
}
|
|
24
|
+
return { $ref: `${SCHEMA_PREFIX}${name}` };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.default = ComponentPool;
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tcG9uZW50LXBvb2wuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb3BlbmFwaS9jb21wb25lbnQtcG9vbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUlBLE1BQU0sYUFBYSxHQUFHLHVCQUF1QixDQUFDO0FBRTlDOzs7O0dBSUc7QUFDSCxNQUFxQixhQUFhO0lBSWhDLFlBQVksUUFBeUI7UUFGcEIsV0FBTSxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7UUFHMUMsSUFBSSxDQUFDLFFBQVEsR0FBRyxRQUFRLENBQUM7SUFDM0IsQ0FBQztJQUVELHlFQUF5RTtJQUN6RSxHQUFHLENBQUMsSUFBWSxFQUFFLFNBQXVCO1FBQ3ZDLE9BQU8sSUFBSSxDQUFDLFFBQVEsQ0FBQyxpQkFBaUIsQ0FBQyxTQUFTLEVBQUUsSUFBSSxFQUFFLFNBQVMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztJQUN6RSxDQUFDO0lBRUQsd0ZBQXdGO0lBQ3hGLEtBQUssQ0FBQyxJQUFZLEVBQUUsTUFBaUI7UUFDbkMsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDM0IsSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQ3JDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3hCLENBQUM7UUFFRCxPQUFPLEVBQUUsSUFBSSxFQUFFLEdBQUcsYUFBYSxHQUFHLElBQUksRUFBRSxFQUFFLENBQUM7SUFDN0MsQ0FBQztDQUNGO0FBdEJELGdDQXNCQyJ9
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FieldType } from '../read-model/capabilities-cache';
|
|
2
|
+
import type { SchemaObject } from 'openapi3-ts/oas31';
|
|
3
|
+
/**
|
|
4
|
+
* Turns a Forest column type into a JSON Schema. Anything unrecognized — a relation marker such as
|
|
5
|
+
* `ManyToOne`, or a type a newer agent introduced — maps to an unconstrained schema rather than a
|
|
6
|
+
* guess, so the document never rejects a value the runtime accepts.
|
|
7
|
+
*/
|
|
8
|
+
export default function toFieldSchema(type: FieldType): SchemaObject;
|
|
9
|
+
//# sourceMappingURL=field-schemas.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = toFieldSchema;
|
|
4
|
+
// Forest primitives, mapped to the closest JSON Schema shape. `Json` maps to no constraint at all,
|
|
5
|
+
// which is honest: any JSON value is accepted there.
|
|
6
|
+
const PRIMITIVE_SCHEMAS = {
|
|
7
|
+
Binary: { type: 'string' },
|
|
8
|
+
Boolean: { type: 'boolean' },
|
|
9
|
+
Date: { type: 'string', format: 'date-time' },
|
|
10
|
+
Dateonly: { type: 'string', format: 'date' },
|
|
11
|
+
Enum: { type: 'string' },
|
|
12
|
+
File: { type: 'string', description: 'A data URI.' },
|
|
13
|
+
Json: {},
|
|
14
|
+
Number: { type: 'number' },
|
|
15
|
+
Point: { type: 'string' },
|
|
16
|
+
String: { type: 'string' },
|
|
17
|
+
Time: { type: 'string' },
|
|
18
|
+
Timeonly: { type: 'string' },
|
|
19
|
+
Uuid: { type: 'string', format: 'uuid' },
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Turns a Forest column type into a JSON Schema. Anything unrecognized — a relation marker such as
|
|
23
|
+
* `ManyToOne`, or a type a newer agent introduced — maps to an unconstrained schema rather than a
|
|
24
|
+
* guess, so the document never rejects a value the runtime accepts.
|
|
25
|
+
*/
|
|
26
|
+
function toFieldSchema(type) {
|
|
27
|
+
if (Array.isArray(type)) {
|
|
28
|
+
const [item] = type;
|
|
29
|
+
return { type: 'array', items: item === undefined ? {} : toFieldSchema(item) };
|
|
30
|
+
}
|
|
31
|
+
// Null-checked, and its elements checked rather than trusted: the payload a column type comes from
|
|
32
|
+
// is cast from untyped JSON, so nothing guarantees the declared shape. A malformed entry is dropped
|
|
33
|
+
// instead of aborting the whole document — one bad field is not worth losing the spec over.
|
|
34
|
+
if (typeof type === 'object' && type !== null && Array.isArray(type.fields)) {
|
|
35
|
+
const named = type.fields.filter(field => typeof field === 'object' && field !== null && typeof field.field === 'string');
|
|
36
|
+
return {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: Object.fromEntries(named.map(field => [field.field, toFieldSchema(field.type)])),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return typeof type === 'string' ? PRIMITIVE_SCHEMAS[type] ?? {} : {};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmllbGQtc2NoZW1hcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vcGVuYXBpL2ZpZWxkLXNjaGVtYXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUEwQkEsZ0NBc0JDO0FBN0NELG1HQUFtRztBQUNuRyxxREFBcUQ7QUFDckQsTUFBTSxpQkFBaUIsR0FBaUM7SUFDdEQsTUFBTSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRTtJQUMxQixPQUFPLEVBQUUsRUFBRSxJQUFJLEVBQUUsU0FBUyxFQUFFO0lBQzVCLElBQUksRUFBRSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLFdBQVcsRUFBRTtJQUM3QyxRQUFRLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUU7SUFDNUMsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRTtJQUN4QixJQUFJLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLFdBQVcsRUFBRSxhQUFhLEVBQUU7SUFDcEQsSUFBSSxFQUFFLEVBQUU7SUFDUixNQUFNLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFO0lBQzFCLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUU7SUFDekIsTUFBTSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRTtJQUMxQixJQUFJLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFO0lBQ3hCLFFBQVEsRUFBRSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUU7SUFDNUIsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFO0NBQ3pDLENBQUM7QUFFRjs7OztHQUlHO0FBQ0gsU0FBd0IsYUFBYSxDQUFDLElBQWU7SUFDbkQsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDeEIsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLElBQUksQ0FBQztRQUVwQixPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsSUFBSSxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztJQUNqRixDQUFDO0lBRUQsbUdBQW1HO0lBQ25HLG9HQUFvRztJQUNwRyw0RkFBNEY7SUFDNUYsSUFBSSxPQUFPLElBQUksS0FBSyxRQUFRLElBQUksSUFBSSxLQUFLLElBQUksSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQzVFLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUM5QixLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLEtBQUssSUFBSSxJQUFJLE9BQU8sS0FBSyxDQUFDLEtBQUssS0FBSyxRQUFRLENBQ3hGLENBQUM7UUFFRixPQUFPO1lBQ0wsSUFBSSxFQUFFLFFBQVE7WUFDZCxVQUFVLEVBQUUsTUFBTSxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLGFBQWEsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQzdGLENBQUM7SUFDSixDQUFDO0lBRUQsT0FBTyxPQUFPLElBQUksS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0FBQ3ZFLENBQUMifQ==
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection, relation and action names are arbitrary customer strings: spaces, dots, slashes,
|
|
3
|
+
* accents. A path segment carries them URL-encoded, but an `operationId` cannot — redocly rejects a
|
|
4
|
+
* non-URL-safe one, and a codegen tool turns it into a method name. So identifiers are sanitized,
|
|
5
|
+
* and the exact name is carried in prose instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeIdentifier(raw: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Hands out unique sanitized identifiers. Sanitizing collapses distinct names (`Mark as paid` and
|
|
10
|
+
* `Mark-as-paid` both become `Mark_as_paid`), so the second one gets a numeric suffix. Callers must
|
|
11
|
+
* feed names in a stable order — the suffix depends on it, and two runs over one schema have to
|
|
12
|
+
* produce the same document.
|
|
13
|
+
*/
|
|
14
|
+
export type Namer = (raw: string) => string;
|
|
15
|
+
export default function createNamer(): Namer;
|
|
16
|
+
//# sourceMappingURL=names.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeIdentifier = sanitizeIdentifier;
|
|
4
|
+
exports.default = createNamer;
|
|
5
|
+
/**
|
|
6
|
+
* Collection, relation and action names are arbitrary customer strings: spaces, dots, slashes,
|
|
7
|
+
* accents. A path segment carries them URL-encoded, but an `operationId` cannot — redocly rejects a
|
|
8
|
+
* non-URL-safe one, and a codegen tool turns it into a method name. So identifiers are sanitized,
|
|
9
|
+
* and the exact name is carried in prose instead.
|
|
10
|
+
*/
|
|
11
|
+
function sanitizeIdentifier(raw) {
|
|
12
|
+
return raw.replace(/[^A-Za-z0-9_]/g, '_') || '_';
|
|
13
|
+
}
|
|
14
|
+
function createNamer() {
|
|
15
|
+
const used = new Set();
|
|
16
|
+
return (raw) => {
|
|
17
|
+
const base = sanitizeIdentifier(raw);
|
|
18
|
+
let candidate = base;
|
|
19
|
+
let counter = 2;
|
|
20
|
+
while (used.has(candidate)) {
|
|
21
|
+
candidate = `${base}_${counter}`;
|
|
22
|
+
counter += 1;
|
|
23
|
+
}
|
|
24
|
+
used.add(candidate);
|
|
25
|
+
return candidate;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmFtZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb3BlbmFwaS9uYW1lcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQU1BLGdEQUVDO0FBVUQsOEJBaUJDO0FBbkNEOzs7OztHQUtHO0FBQ0gsU0FBZ0Isa0JBQWtCLENBQUMsR0FBVztJQUM1QyxPQUFPLEdBQUcsQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLEVBQUUsR0FBRyxDQUFDLElBQUksR0FBRyxDQUFDO0FBQ25ELENBQUM7QUFVRCxTQUF3QixXQUFXO0lBQ2pDLE1BQU0sSUFBSSxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFFL0IsT0FBTyxDQUFDLEdBQVcsRUFBVSxFQUFFO1FBQzdCLE1BQU0sSUFBSSxHQUFHLGtCQUFrQixDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3JDLElBQUksU0FBUyxHQUFHLElBQUksQ0FBQztRQUNyQixJQUFJLE9BQU8sR0FBRyxDQUFDLENBQUM7UUFFaEIsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUM7WUFDM0IsU0FBUyxHQUFHLEdBQUcsSUFBSSxJQUFJLE9BQU8sRUFBRSxDQUFDO1lBQ2pDLE9BQU8sSUFBSSxDQUFDLENBQUM7UUFDZixDQUFDO1FBRUQsSUFBSSxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUVwQixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDLENBQUM7QUFDSixDQUFDIn0=
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Unfolding } from './unfolding';
|
|
1
2
|
import type { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
2
3
|
export declare const OPENAPI_VERSION = "3.1.0";
|
|
3
4
|
export declare const ROUTE_PREFIX = "/agent/v1";
|
|
4
|
-
export declare function generateOpenApiDocument(version: string): OpenAPIObject;
|
|
5
|
+
export declare function generateOpenApiDocument(version: string, unfolding?: Unfolding): OpenAPIObject;
|
|
5
6
|
export declare function serializeOpenApi(document: OpenAPIObject): string;
|
|
6
7
|
//# sourceMappingURL=openapi-document.d.ts.map
|
|
@@ -7,7 +7,9 @@ exports.ROUTE_PREFIX = exports.OPENAPI_VERSION = void 0;
|
|
|
7
7
|
exports.generateOpenApiDocument = generateOpenApiDocument;
|
|
8
8
|
exports.serializeOpenApi = serializeOpenApi;
|
|
9
9
|
const zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi");
|
|
10
|
+
const component_pool_1 = __importDefault(require("./component-pool"));
|
|
10
11
|
const schemas_1 = require("./schemas");
|
|
12
|
+
const unfolded_paths_1 = __importDefault(require("./unfolded-paths"));
|
|
11
13
|
const zod_openapi_1 = require("./zod-openapi");
|
|
12
14
|
const body_limit_1 = __importDefault(require("../http/body-limit"));
|
|
13
15
|
exports.OPENAPI_VERSION = '3.1.0';
|
|
@@ -31,39 +33,53 @@ const ERROR_STATUSES = {
|
|
|
31
33
|
};
|
|
32
34
|
const UNSUPPORTED_RESULT_DESCRIPTION = 'Either the BFF runs without an agent configured, or the action returned a result shape the ' +
|
|
33
35
|
'BFF cannot normalize. The second case carries no message field.';
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const ERROR_RESPONSE_REF = '#/components/schemas/ErrorResponse';
|
|
37
|
+
const MESSAGELESS_ERROR_RESPONSE_REF = '#/components/schemas/MessagelessErrorResponse';
|
|
38
|
+
const UNSUPPORTED_ACTION_RESULT_COMPONENT = 'UnsupportedActionResult';
|
|
39
|
+
const RETRY_AFTER_HEADER = {
|
|
40
|
+
'Retry-After': {
|
|
41
|
+
description: 'Seconds to wait before retrying. Set when the API key could not be resolved.',
|
|
42
|
+
required: false,
|
|
43
|
+
schema: { type: 'integer' },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
// Every error body is identical across paths, and their descriptions are long: inlining them costs
|
|
47
|
+
// ~2.8kB per path, which the unfolded document multiplies by every collection, relation and action.
|
|
48
|
+
// Registering them once as components keeps a path's error block to a dozen refs.
|
|
49
|
+
function registerErrorResponses(registry, statuses, withActionResults) {
|
|
50
|
+
registry.register('ErrorResponse', schemas_1.ErrorResponseSchema);
|
|
51
|
+
const byStatus = {};
|
|
52
|
+
statuses.forEach(status => {
|
|
36
53
|
const description = ERROR_STATUSES[status];
|
|
37
54
|
if (!description)
|
|
38
55
|
throw new Error(`No OpenAPI description for error status ${status}`);
|
|
39
|
-
|
|
40
|
-
return [
|
|
41
|
-
status,
|
|
42
|
-
{
|
|
43
|
-
description: UNSUPPORTED_RESULT_DESCRIPTION,
|
|
44
|
-
content: {
|
|
45
|
-
'application/json': {
|
|
46
|
-
schema: zod_openapi_1.z.union([schemas_1.ErrorResponseSchema, schemas_1.MessagelessErrorResponseSchema]),
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
|
-
const response = {
|
|
56
|
+
byStatus[status] = registry.registerComponent('responses', `Error${status}`, {
|
|
53
57
|
description,
|
|
54
|
-
content: { 'application/json': { schema:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
content: { 'application/json': { schema: { $ref: ERROR_RESPONSE_REF } } },
|
|
59
|
+
...(status === '503' ? { headers: RETRY_AFTER_HEADER } : {}),
|
|
60
|
+
}).ref;
|
|
61
|
+
});
|
|
62
|
+
// A document with no action path must not carry this response, nor the messageless body it
|
|
63
|
+
// references: an unreferenced component trips redocly's unused-component rule.
|
|
64
|
+
if (!withActionResults)
|
|
65
|
+
return { byStatus, unsupportedActionResult: byStatus['501'] };
|
|
66
|
+
registry.register('MessagelessErrorResponse', schemas_1.MessagelessErrorResponseSchema);
|
|
67
|
+
const unsupportedActionResult = registry.registerComponent('responses', UNSUPPORTED_ACTION_RESULT_COMPONENT, {
|
|
68
|
+
description: UNSUPPORTED_RESULT_DESCRIPTION,
|
|
69
|
+
content: {
|
|
70
|
+
'application/json': {
|
|
71
|
+
schema: {
|
|
72
|
+
anyOf: [{ $ref: ERROR_RESPONSE_REF }, { $ref: MESSAGELESS_ERROR_RESPONSE_REF }],
|
|
62
73
|
},
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}).ref;
|
|
77
|
+
return { byStatus, unsupportedActionResult };
|
|
78
|
+
}
|
|
79
|
+
function errorResponses(refs, executeResults) {
|
|
80
|
+
return executeResults
|
|
81
|
+
? { ...refs.byStatus, 501: refs.unsupportedActionResult }
|
|
82
|
+
: { ...refs.byStatus };
|
|
67
83
|
}
|
|
68
84
|
const DATA_ERRORS = [
|
|
69
85
|
'400',
|
|
@@ -148,17 +164,42 @@ const PARAM_DESCRIPTIONS = {
|
|
|
148
164
|
function buildParams(names) {
|
|
149
165
|
return zod_openapi_1.z.object(Object.fromEntries(names.map(name => [name, zod_openapi_1.z.string().openapi({ description: PARAM_DESCRIPTIONS[name] })])));
|
|
150
166
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
167
|
+
// Registered as a parameter component rather than inlined: it is the same header on every path, and
|
|
168
|
+
// its description costs a quarter of a path once the error responses are refs.
|
|
169
|
+
const TIMEZONE_HEADER_COMPONENT = 'XForestTimezone';
|
|
170
|
+
const TIMEZONE_HEADER = zod_openapi_1.z
|
|
171
|
+
.string()
|
|
172
|
+
.optional()
|
|
173
|
+
.openapi({
|
|
174
|
+
param: { name: 'X-Forest-Timezone', in: 'header' },
|
|
175
|
+
description: 'An IANA timezone. Takes precedence over the `timezone` body field; the BFF default ' +
|
|
176
|
+
'applies when neither is sent.',
|
|
159
177
|
});
|
|
160
|
-
|
|
178
|
+
const SHARED_DESCRIPTION = 'The timezone is resolved from the `X-Forest-Timezone` header first, then a `timezone` body ' +
|
|
179
|
+
'field, then the BFF default when one is configured. A deployment without a default rejects a ' +
|
|
180
|
+
'request carrying neither with 400 missing_timezone, so send one of the two to be safe. Sending ' +
|
|
181
|
+
'a content type other than application/json is not an error: a form-urlencoded body is parsed ' +
|
|
182
|
+
'like JSON, while any other content type is read as absent, which silently drops any filter, ' +
|
|
183
|
+
'sort, or page.';
|
|
184
|
+
const GENERIC_DESCRIPTION = 'Paths are generic: one per operation, with the collection, relation and action passed as path ' +
|
|
185
|
+
'segments, and no field enumerated. This is the fallback form — a deployment configured to ' +
|
|
186
|
+
'reach its Forest schema and its agent unfolds one path per real collection, relation and ' +
|
|
187
|
+
'action instead, each carrying its own field set.';
|
|
188
|
+
const UNFOLDED_DESCRIPTION = 'Paths are unfolded: one per exposed collection, to-many relation and action, each carrying the ' +
|
|
189
|
+
"collection's real field set. The RUNTIME routes stay generic — a path here is the generic " +
|
|
190
|
+
'route with its segments already filled in, so the collection, relation and action segments are ' +
|
|
191
|
+
'the exact schema names, URL-encoded. Every operationId is sanitized to stay usable as a ' +
|
|
192
|
+
'method name, so the exact name lives in the summary and description. A path whose fields are ' +
|
|
193
|
+
'not enumerated says so in its request description: the document was built without that ' +
|
|
194
|
+
"collection's capabilities. Only to-many relations appear; a to-one or polymorphic relation has " +
|
|
195
|
+
'no list or count route. This document describes the whole exposed schema regardless of the ' +
|
|
196
|
+
'caller: it is not filtered by the permissions of whoever fetched it.';
|
|
197
|
+
function generateOpenApiDocument(version, unfolding) {
|
|
161
198
|
const registry = new zod_to_openapi_1.OpenAPIRegistry();
|
|
199
|
+
const hasActions = unfolding === undefined ||
|
|
200
|
+
unfolding.collections.some(collection => collection.actions.length > 0);
|
|
201
|
+
const errorRefs = registerErrorResponses(registry, DATA_ERRORS, hasActions);
|
|
202
|
+
const timezoneHeader = [registry.registerParameter(TIMEZONE_HEADER_COMPONENT, TIMEZONE_HEADER)];
|
|
162
203
|
registry.registerComponent('securitySchemes', SESSION_SCHEME, {
|
|
163
204
|
type: 'http',
|
|
164
205
|
scheme: 'bearer',
|
|
@@ -172,44 +213,49 @@ function generateOpenApiDocument(version) {
|
|
|
172
213
|
name: 'X-Forest-Bff-Key',
|
|
173
214
|
description: 'Mode 2: a BFF API key. Never send both this and an Authorization header.',
|
|
174
215
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
summary: route.summary,
|
|
216
|
+
if (unfolding) {
|
|
217
|
+
(0, unfolded_paths_1.default)({
|
|
218
|
+
registry,
|
|
219
|
+
pool: new component_pool_1.default(registry),
|
|
220
|
+
prefix: exports.ROUTE_PREFIX,
|
|
181
221
|
security: SECURITY,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
222
|
+
timezoneHeader,
|
|
223
|
+
errorResponses: executeResults => errorResponses(errorRefs, executeResults),
|
|
224
|
+
}, unfolding);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
ROUTES.forEach(route => {
|
|
228
|
+
registry.registerPath({
|
|
229
|
+
method: 'post',
|
|
230
|
+
path: `${exports.ROUTE_PREFIX}${route.path}`,
|
|
231
|
+
operationId: route.operationId,
|
|
232
|
+
summary: route.summary,
|
|
233
|
+
security: SECURITY,
|
|
234
|
+
request: {
|
|
235
|
+
params: buildParams(route.params),
|
|
236
|
+
headers: timezoneHeader,
|
|
237
|
+
body: {
|
|
238
|
+
required: route.bodyRequired === true,
|
|
239
|
+
content: { 'application/json': { schema: route.request } },
|
|
240
|
+
},
|
|
188
241
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
responses: {
|
|
243
|
+
200: {
|
|
244
|
+
description: route.responseDescription,
|
|
245
|
+
content: { 'application/json': { schema: route.response } },
|
|
246
|
+
},
|
|
247
|
+
...errorResponses(errorRefs, route.executeResults === true),
|
|
194
248
|
},
|
|
195
|
-
|
|
196
|
-
},
|
|
249
|
+
});
|
|
197
250
|
});
|
|
198
|
-
}
|
|
251
|
+
}
|
|
199
252
|
return new zod_to_openapi_1.OpenApiGeneratorV31(registry.definitions).generateDocument({
|
|
200
253
|
openapi: exports.OPENAPI_VERSION,
|
|
201
254
|
info: {
|
|
202
255
|
title: 'Forest Admin BFF',
|
|
203
256
|
version,
|
|
204
257
|
license: { name: 'GPL-3.0', url: 'https://www.gnu.org/licenses/gpl-3.0.html' },
|
|
205
|
-
description:
|
|
206
|
-
'action passed as path segments. The timezone is resolved from the `X-Forest-Timezone` ' +
|
|
207
|
-
'header first, then a `timezone` body field, then the BFF default when one is ' +
|
|
208
|
-
'configured. A deployment without a default rejects a request carrying neither with ' +
|
|
209
|
-
'400 missing_timezone, so send one of the two to be safe. Sending a content type other ' +
|
|
210
|
-
'than application/json is not an error: a form-urlencoded body is parsed like JSON, ' +
|
|
211
|
-
'while any other content type is read as absent, which silently drops any filter, ' +
|
|
212
|
-
'sort, or page.',
|
|
258
|
+
description: `${unfolding ? UNFOLDED_DESCRIPTION : GENERIC_DESCRIPTION} ${SHARED_DESCRIPTION}`,
|
|
213
259
|
},
|
|
214
260
|
servers: [{ url: '/' }],
|
|
215
261
|
});
|
|
@@ -217,4 +263,4 @@ function generateOpenApiDocument(version) {
|
|
|
217
263
|
function serializeOpenApi(document) {
|
|
218
264
|
return JSON.stringify(document, null, 2);
|
|
219
265
|
}
|
|
220
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
266
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFwaS1kb2N1bWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vcGVuYXBpL29wZW5hcGktZG9jdW1lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBMlFBLDBEQTBFQztBQUVELDRDQUVDO0FBdFZELG1FQUFzRjtBQUV0RixzRUFBNkM7QUFDN0MsdUNBVW1CO0FBQ25CLHNFQUFxRDtBQUNyRCwrQ0FBa0M7QUFDbEMsb0VBQTRDO0FBRS9CLFFBQUEsZUFBZSxHQUFHLE9BQU8sQ0FBQztBQUMxQixRQUFBLFlBQVksR0FBRyxXQUFXLENBQUM7QUFFeEMsTUFBTSxjQUFjLEdBQUcsWUFBWSxDQUFDO0FBQ3BDLE1BQU0sY0FBYyxHQUFHLFdBQVcsQ0FBQztBQUVuQyxNQUFNLFFBQVEsR0FBRyxDQUFDLEVBQUUsQ0FBQyxjQUFjLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBRTVDLE1BQU0sY0FBYyxHQUEyQjtJQUM3QyxHQUFHLEVBQUUseVFBQXlRO0lBQzlRLEdBQUcsRUFBRSwwQ0FBMEM7SUFDL0MsR0FBRyxFQUFFLDBPQUEwTztJQUMvTyxHQUFHLEVBQUUseUNBQXlDO0lBQzlDLEdBQUcsRUFBRSw2Q0FBNkMsb0JBQVUsRUFBRTtJQUM5RCxHQUFHLEVBQUUsMlZBQTJWO0lBQ2hXLEdBQUcsRUFBRSxrRUFBa0U7SUFDdkUsR0FBRyxFQUFFLG9DQUFvQztJQUN6QyxHQUFHLEVBQUUsMkRBQTJEO0lBQ2hFLEdBQUcsRUFBRSxpRkFBaUY7SUFDdEYsR0FBRyxFQUFFLGdDQUFnQztJQUNyQyxHQUFHLEVBQUUsaUdBQWlHO0NBQ3ZHLENBQUM7QUFFRixNQUFNLDhCQUE4QixHQUNsQyw2RkFBNkY7SUFDN0YsaUVBQWlFLENBQUM7QUFFcEUsTUFBTSxrQkFBa0IsR0FBRyxvQ0FBb0MsQ0FBQztBQUNoRSxNQUFNLDhCQUE4QixHQUFHLCtDQUErQyxDQUFDO0FBRXZGLE1BQU0sbUNBQW1DLEdBQUcseUJB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
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { UnfoldSource } from './unfolded-document';
|
|
1
2
|
import type { Middleware } from 'koa';
|
|
2
3
|
export declare const OPENAPI_PATH = "/agent/openapi.json";
|
|
3
4
|
export interface OpenApiRoutesOptions {
|
|
4
5
|
version: string;
|
|
5
6
|
enabled: boolean;
|
|
7
|
+
/** Absent (no agent or no read-model configuration) serves the generic document. */
|
|
8
|
+
source?: UnfoldSource;
|
|
6
9
|
}
|
|
7
|
-
export default function createOpenApiRoutes({ version, enabled, }: OpenApiRoutesOptions): Middleware;
|
|
10
|
+
export default function createOpenApiRoutes({ version, enabled, source, }: OpenApiRoutesOptions): Middleware;
|
|
8
11
|
//# sourceMappingURL=openapi-routes.d.ts.map
|
|
@@ -1,29 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.OPENAPI_PATH = void 0;
|
|
4
7
|
exports.default = createOpenApiRoutes;
|
|
5
8
|
const openapi_document_1 = require("./openapi-document");
|
|
9
|
+
const unfolded_document_1 = __importDefault(require("./unfolded-document"));
|
|
10
|
+
const unfolding_1 = require("./unfolding");
|
|
11
|
+
const agent_route_helpers_1 = require("../http/agent-route-helpers");
|
|
6
12
|
const bff_local_errors_1 = require("../http/bff-local-errors");
|
|
7
13
|
exports.OPENAPI_PATH = '/agent/openapi.json';
|
|
8
14
|
const READ_METHODS = new Set(['GET', 'HEAD']);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const MAX_GENERATION_RETRIES = 3;
|
|
16
|
+
function createOpenApiRoutes({ version, enabled, source, }) {
|
|
17
|
+
const generic = enabled && !source ? (0, openapi_document_1.serializeOpenApi)((0, openapi_document_1.generateOpenApiDocument)(version)) : undefined;
|
|
18
|
+
// Memoized on the read-model identity: the store builds a new one per schema generation, so the
|
|
19
|
+
// document is rebuilt exactly when the schema it describes changed.
|
|
20
|
+
let unfolded;
|
|
21
|
+
async function resolveDocument(ctx, attemptsLeft = MAX_GENERATION_RETRIES) {
|
|
22
|
+
if (!source)
|
|
23
|
+
return generic;
|
|
24
|
+
if (attemptsLeft <= 0) {
|
|
25
|
+
throw new Error('Schema generation kept changing while building the OpenAPI document');
|
|
15
26
|
}
|
|
16
|
-
|
|
27
|
+
// Before the schema is even read, so a caller with no agent credentials cannot make the BFF
|
|
28
|
+
// fetch anything: the unfolded document needs a token to ask the agent for capabilities.
|
|
29
|
+
const token = (0, agent_route_helpers_1.requireAgentToken)(ctx);
|
|
30
|
+
const readModel = await (0, agent_route_helpers_1.resolveReadModel)(source.store);
|
|
31
|
+
if (unfolded?.readModel === readModel)
|
|
32
|
+
return unfolded.document;
|
|
33
|
+
const { document, unfolding } = await (0, unfolded_document_1.default)(source, readModel, token, version);
|
|
34
|
+
// A schema refresh landing during the capabilities fan-out mixes the new generation's field sets
|
|
35
|
+
// into this generation's collections, relations and actions. Such a document must not be served
|
|
36
|
+
// nor cached — a consumer would generate a client from a self-contradictory schema. Bounded like
|
|
37
|
+
// `ReadModelStore.getCapabilities`, and driven by the 24h schema TTL rather than request volume.
|
|
38
|
+
if ((await (0, agent_route_helpers_1.resolveReadModel)(source.store)) !== readModel) {
|
|
39
|
+
return resolveDocument(ctx, attemptsLeft - 1);
|
|
40
|
+
}
|
|
41
|
+
// Only a complete document is memoized. The memo key is the schema generation, which moves on a
|
|
42
|
+
// 24h TTL, so caching a document built while the agent was briefly down would keep serving a
|
|
43
|
+
// field-less spec for a day — and longer still if a later schema refresh fails without bumping
|
|
44
|
+
// the revision. `CapabilitiesCache` deliberately caches successes only; this keeps that true
|
|
45
|
+
// end to end, at the cost of re-running the fan-out until the agent answers again.
|
|
46
|
+
if (!(0, unfolding_1.hasDegradedCollection)(unfolding)) {
|
|
47
|
+
unfolded = { readModel, document };
|
|
48
|
+
}
|
|
49
|
+
return document;
|
|
50
|
+
}
|
|
51
|
+
return async function openApiRoutes(ctx, next) {
|
|
52
|
+
if (ctx.path !== exports.OPENAPI_PATH || !READ_METHODS.has(ctx.method)) {
|
|
17
53
|
await next();
|
|
18
54
|
return;
|
|
19
55
|
}
|
|
20
|
-
if (
|
|
56
|
+
if (!enabled) {
|
|
21
57
|
throw (0, bff_local_errors_1.openapiDisabled)();
|
|
22
58
|
}
|
|
59
|
+
const document = await resolveDocument(ctx);
|
|
23
60
|
ctx.status = 200;
|
|
24
61
|
ctx.type = 'application/json';
|
|
25
62
|
ctx.set('Cache-Control', 'no-store');
|
|
26
63
|
ctx.body = document;
|
|
27
64
|
};
|
|
28
65
|
}
|
|
29
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
66
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFwaS1yb3V0ZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb3BlbmFwaS9vcGVuYXBpLXJvdXRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUF1QkEsc0NBcUVDO0FBeEZELHlEQUErRTtBQUMvRSw0RUFBd0Q7QUFDeEQsMkNBQW9EO0FBQ3BELHFFQUFrRjtBQUNsRiwrREFBMkQ7QUFFOUMsUUFBQSxZQUFZLEdBQUcscUJBQXFCLENBQUM7QUFFbEQsTUFBTSxZQUFZLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQztBQUU5QyxNQUFNLHNCQUFzQixHQUFHLENBQUMsQ0FBQztBQVNqQyxTQUF3QixtQkFBbUIsQ0FBQyxFQUMxQyxPQUFPLEVBQ1AsT0FBTyxFQUNQLE1BQU0sR0FDZTtJQUNyQixNQUFNLE9BQU8sR0FDWCxPQUFPLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUEsbUNBQWdCLEVBQUMsSUFBQSwwQ0FBdUIsRUFBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7SUFFdEYsZ0dBQWdHO0lBQ2hHLG9FQUFvRTtJQUNwRSxJQUFJLFFBQWdFLENBQUM7SUFFckUsS0FBSyxVQUFVLGVBQWUsQ0FDNUIsR0FBOEIsRUFDOUIsWUFBWSxHQUFHLHNCQUFzQjtRQUVyQyxJQUFJLENBQUMsTUFBTTtZQUFFLE9BQU8sT0FBaUIsQ0FBQztRQUV0QyxJQUFJLFlBQVksSUFBSSxDQUFDLEVBQUUsQ0FBQztZQUN0QixNQUFNLElBQUksS0FBSyxDQUFDLHFFQUFxRSxDQUFDLENBQUM7UUFDekYsQ0FBQztRQUVELDRGQUE0RjtRQUM1Rix5RkFBeUY7UUFDekYsTUFBTSxLQUFLLEdBQUcsSUFBQSx1Q0FBaUIsRUFBQyxHQUFHLENBQUMsQ0FBQztRQUNyQyxNQUFNLFNBQVMsR0FBRyxNQUFNLElBQUEsc0NBQWdCLEVBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBRXZELElBQUksUUFBUSxFQUFFLFNBQVMsS0FBSyxTQUFTO1lBQUUsT0FBTyxRQUFRLENBQUMsUUFBUSxDQUFDO1FBRWhFLE1BQU0sRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLEdBQUcsTUFBTSxJQUFBLDJCQUFxQixFQUFDLE1BQU0sRUFBRSxTQUFTLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBRS9GLGlHQUFpRztRQUNqRyxnR0FBZ0c7UUFDaEcsaUdBQWlHO1FBQ2pHLGlHQUFpRztRQUNqRyxJQUFJLENBQUMsTUFBTSxJQUFBLHNDQUFnQixFQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLFNBQVMsRUFBRSxDQUFDO1lBQ3pELE9BQU8sZUFBZSxDQUFDLEdBQUcsRUFBRSxZQUFZLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDaEQsQ0FBQztRQUVELGdHQUFnRztRQUNoRyw2RkFBNkY7UUFDN0YsK0ZBQStGO1FBQy9GLDZGQUE2RjtRQUM3RixtRkFBbUY7UUFDbkYsSUFBSSxDQUFDLElBQUEsaUNBQXFCLEVBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztZQUN0QyxRQUFRLEdBQUcsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLENBQUM7UUFDckMsQ0FBQztRQUVELE9BQU8sUUFBUSxDQUFDO0lBQ2xCLENBQUM7SUFFRCxPQUFPLEtBQUssVUFBVSxhQUFhLENBQUMsR0FBRyxFQUFFLElBQUk7UUFDM0MsSUFBSSxHQUFHLENBQUMsSUFBSSxLQUFLLG9CQUFZLElBQUksQ0FBQyxZQUFZLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1lBQy9ELE1BQU0sSUFBSSxFQUFFLENBQUM7WUFFYixPQUFPO1FBQ1QsQ0FBQztRQUVELElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNiLE1BQU0sSUFBQSxrQ0FBZSxHQUFFLENBQUM7UUFDMUIsQ0FBQztRQUVELE1BQU0sUUFBUSxHQUFHLE1BQU0sZUFBZSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRTVDLEdBQUcsQ0FBQyxNQUFNLEdBQUcsR0FBRyxDQUFDO1FBQ2pCLEdBQUcsQ0FBQyxJQUFJLEdBQUcsa0JBQWtCLENBQUM7UUFDOUIsR0FBRyxDQUFDLEdBQUcsQ0FBQyxlQUFlLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFDckMsR0FBRyxDQUFDLElBQUksR0FBRyxRQUFRLENBQUM7SUFDdEIsQ0FBQyxDQUFDO0FBQ0osQ0FBQyJ9
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { z } from './zod-openapi';
|
|
2
2
|
declare const OPERATORS: [string, ...string[]];
|
|
3
3
|
declare const ConditionTreeSchema: z.ZodType;
|
|
4
|
+
export declare const SortClauseSchema: z.ZodObject<{
|
|
5
|
+
field: z.ZodString;
|
|
6
|
+
direction: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
asc: "asc";
|
|
8
|
+
desc: "desc";
|
|
9
|
+
}>>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare const PageSchema: z.ZodObject<{
|
|
12
|
+
limit: z.ZodNumber;
|
|
13
|
+
offset: z.ZodNumber;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export declare const TimezoneSchema: z.ZodString;
|
|
4
16
|
export declare const ListRequestSchema: z.ZodObject<{
|
|
5
17
|
filter: z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
6
18
|
projection: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -21,6 +33,7 @@ export declare const CountRequestSchema: z.ZodObject<{
|
|
|
21
33
|
filter: z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
22
34
|
timezone: z.ZodOptional<z.ZodString>;
|
|
23
35
|
}, z.core.$strip>;
|
|
36
|
+
declare const ParentIdSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
24
37
|
export declare const RelationListRequestSchema: z.ZodObject<{
|
|
25
38
|
filter: z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
26
39
|
projection: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -81,5 +94,5 @@ export declare const MessagelessErrorResponseSchema: z.ZodObject<{
|
|
|
81
94
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
82
95
|
}, z.core.$strip>;
|
|
83
96
|
}, z.core.$strip>;
|
|
84
|
-
export { ConditionTreeSchema, OPERATORS };
|
|
97
|
+
export { ConditionTreeSchema, ParentIdSchema, OPERATORS };
|
|
85
98
|
//# sourceMappingURL=schemas.d.ts.map
|