@forestadmin/agent-bff 1.5.0 → 1.7.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/dist/adapters/console-metrics.d.ts +4 -0
- package/dist/adapters/console-metrics.js +18 -0
- package/dist/adapters/safe-metrics.d.ts +8 -0
- package/dist/adapters/safe-metrics.js +29 -0
- package/dist/cli-core.js +17 -2
- package/dist/data/agent-data-client.d.ts +17 -0
- package/dist/data/agent-data-client.js +32 -0
- package/dist/data/agent-query.d.ts +36 -0
- package/dist/data/agent-query.js +144 -0
- package/dist/data/data-routes-middleware.d.ts +12 -0
- package/dist/data/data-routes-middleware.js +159 -0
- package/dist/data/pack-id.d.ts +9 -0
- package/dist/data/pack-id.js +37 -0
- package/dist/data/response-mappers.d.ts +20 -0
- package/dist/data/response-mappers.js +38 -0
- package/dist/http/agent-error-mapper.d.ts +18 -0
- package/dist/http/agent-error-mapper.js +114 -0
- package/dist/http/bff-local-errors.d.ts +12 -0
- package/dist/http/bff-local-errors.js +44 -0
- package/dist/ports/metrics-port.d.ts +6 -0
- package/dist/ports/metrics-port.js +3 -0
- package/dist/read-model/action-endpoint-resolver.d.ts +23 -0
- package/dist/read-model/action-endpoint-resolver.js +36 -0
- package/dist/read-model/agent-capabilities-fetcher.d.ts +11 -0
- package/dist/read-model/agent-capabilities-fetcher.js +13 -0
- package/dist/read-model/capabilities-cache.d.ts +27 -0
- package/dist/read-model/capabilities-cache.js +47 -0
- package/dist/read-model/create-read-model.d.ts +17 -0
- package/dist/read-model/create-read-model.js +24 -0
- package/dist/read-model/errors.d.ts +5 -0
- package/dist/read-model/errors.js +11 -0
- package/dist/read-model/forest-schema-client.d.ts +14 -0
- package/dist/read-model/forest-schema-client.js +13 -0
- package/dist/read-model/read-model-store.d.ts +20 -0
- package/dist/read-model/read-model-store.js +43 -0
- package/dist/read-model/read-model.d.ts +39 -0
- package/dist/read-model/read-model.js +105 -0
- package/dist/read-model/schema-cache.d.ts +35 -0
- package/dist/read-model/schema-cache.js +82 -0
- package/package.json +2 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PrimaryKeyField } from '../read-model/read-model';
|
|
2
|
+
export interface ForestRecordMetadata {
|
|
3
|
+
collection: string;
|
|
4
|
+
primaryKey: Record<string, string | number>;
|
|
5
|
+
}
|
|
6
|
+
export interface ListResponse {
|
|
7
|
+
data: Array<Record<string, unknown> & {
|
|
8
|
+
__forest: ForestRecordMetadata;
|
|
9
|
+
}>;
|
|
10
|
+
meta: {
|
|
11
|
+
countStatus: 'not_requested';
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface CountResponse {
|
|
15
|
+
count: number | null;
|
|
16
|
+
countStatus: 'available' | 'deactivated';
|
|
17
|
+
}
|
|
18
|
+
export declare function mapListResponse(collection: string, records: Record<string, unknown>[], primaryKeys: PrimaryKeyField[]): ListResponse;
|
|
19
|
+
export declare function mapCountResponse(raw: unknown): CountResponse;
|
|
20
|
+
//# sourceMappingURL=response-mappers.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.mapListResponse = mapListResponse;
|
|
7
|
+
exports.mapCountResponse = mapCountResponse;
|
|
8
|
+
const pack_id_1 = __importDefault(require("./pack-id"));
|
|
9
|
+
const bff_local_errors_1 = require("../http/bff-local-errors");
|
|
10
|
+
const DEACTIVATED = 'deactivated';
|
|
11
|
+
function mapListResponse(collection, records, primaryKeys) {
|
|
12
|
+
const data = records.map(record => {
|
|
13
|
+
if (record.id === undefined || record.id === null) {
|
|
14
|
+
throw (0, bff_local_errors_1.mappingError)('Agent record is missing its id');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
...record,
|
|
18
|
+
__forest: {
|
|
19
|
+
collection,
|
|
20
|
+
primaryKey: (0, pack_id_1.default)(String(record.id), primaryKeys),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return { data, meta: { countStatus: 'not_requested' } };
|
|
25
|
+
}
|
|
26
|
+
// meta.count is the only deactivation signal the agent exposes.
|
|
27
|
+
function mapCountResponse(raw) {
|
|
28
|
+
const body = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
29
|
+
if (body.meta?.count === DEACTIVATED) {
|
|
30
|
+
return { count: null, countStatus: 'deactivated' };
|
|
31
|
+
}
|
|
32
|
+
if (typeof body.count === 'number' && Number.isFinite(body.count)) {
|
|
33
|
+
return { count: body.count, countStatus: 'available' };
|
|
34
|
+
}
|
|
35
|
+
// Neither schema metadata nor a usable raw payload: never silently report 0.
|
|
36
|
+
throw (0, bff_local_errors_1.mappingError)('Agent count payload has no numeric count and no deactivated marker');
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVzcG9uc2UtbWFwcGVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kYXRhL3Jlc3BvbnNlLW1hcHBlcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFzQkEsMENBb0JDO0FBR0QsNENBZ0JDO0FBM0RELHdEQUF5QztBQUN6QywrREFBd0Q7QUFpQnhELE1BQU0sV0FBVyxHQUFHLGFBQWEsQ0FBQztBQUVsQyxTQUFnQixlQUFlLENBQzdCLFVBQWtCLEVBQ2xCLE9BQWtDLEVBQ2xDLFdBQThCO0lBRTlCLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUU7UUFDaEMsSUFBSSxNQUFNLENBQUMsRUFBRSxLQUFLLFNBQVMsSUFBSSxNQUFNLENBQUMsRUFBRSxLQUFLLElBQUksRUFBRSxDQUFDO1lBQ2xELE1BQU0sSUFBQSwrQkFBWSxFQUFDLGdDQUFnQyxDQUFDLENBQUM7UUFDdkQsQ0FBQztRQUVELE9BQU87WUFDTCxHQUFHLE1BQU07WUFDVCxRQUFRLEVBQUU7Z0JBQ1IsVUFBVTtnQkFDVixVQUFVLEVBQUUsSUFBQSxpQkFBZ0IsRUFBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxFQUFFLFdBQVcsQ0FBQzthQUM3RDtTQUNGLENBQUM7SUFDSixDQUFDLENBQUMsQ0FBQztJQUVILE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEVBQUUsV0FBVyxFQUFFLGVBQWUsRUFBRSxFQUFFLENBQUM7QUFDMUQsQ0FBQztBQUVELGdFQUFnRTtBQUNoRSxTQUFnQixnQkFBZ0IsQ0FBQyxHQUFZO0lBQzNDLE1BQU0sSUFBSSxHQUFHLENBQUMsT0FBTyxHQUFHLEtBQUssUUFBUSxJQUFJLEdBQUcsS0FBSyxJQUFJLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUcvRCxDQUFDO0lBRUYsSUFBSSxJQUFJLENBQUMsSUFBSSxFQUFFLEtBQUssS0FBSyxXQUFXLEVBQUUsQ0FBQztRQUNyQyxPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLENBQUM7SUFDckQsQ0FBQztJQUVELElBQUksT0FBTyxJQUFJLENBQUMsS0FBSyxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ2xFLE9BQU8sRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLEtBQUssRUFBRSxXQUFXLEVBQUUsV0FBVyxFQUFFLENBQUM7SUFDekQsQ0FBQztJQUVELDZFQUE2RTtJQUM3RSxNQUFNLElBQUEsK0JBQVksRUFBQyxvRUFBb0UsQ0FBQyxDQUFDO0FBQzNGLENBQUMifQ==
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Logger } from '../ports/logger-port';
|
|
2
|
+
import { BffHttpError } from './bff-http-error';
|
|
3
|
+
declare const TYPE_INVALID_REQUEST = "invalid_request";
|
|
4
|
+
declare const TYPE_VALIDATION_ERROR = "validation_error";
|
|
5
|
+
declare const TYPE_UNAUTHORIZED = "unauthorized";
|
|
6
|
+
declare const TYPE_FORBIDDEN = "forbidden";
|
|
7
|
+
declare const TYPE_NOT_FOUND = "not_found";
|
|
8
|
+
declare const TYPE_UNPROCESSABLE = "unprocessable_entity";
|
|
9
|
+
declare const TYPE_TOO_MANY_REQUESTS = "too_many_requests";
|
|
10
|
+
declare const TYPE_NETWORK_ERROR = "network_error";
|
|
11
|
+
declare const TYPE_AGENT_UNAVAILABLE = "agent_unavailable";
|
|
12
|
+
type BffErrorType = typeof TYPE_INVALID_REQUEST | typeof TYPE_VALIDATION_ERROR | typeof TYPE_UNAUTHORIZED | typeof TYPE_FORBIDDEN | typeof TYPE_NOT_FOUND | typeof TYPE_UNPROCESSABLE | typeof TYPE_TOO_MANY_REQUESTS | typeof TYPE_NETWORK_ERROR | typeof TYPE_AGENT_UNAVAILABLE;
|
|
13
|
+
export declare const AGENT_ERROR_TYPE_MAP: Record<string, BffErrorType>;
|
|
14
|
+
export declare function mapAgentError(error: unknown, { logger }: {
|
|
15
|
+
logger: Logger;
|
|
16
|
+
}): BffHttpError;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=agent-error-mapper.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_ERROR_TYPE_MAP = void 0;
|
|
4
|
+
exports.mapAgentError = mapAgentError;
|
|
5
|
+
const agent_client_1 = require("@forestadmin/agent-client");
|
|
6
|
+
const bff_http_error_1 = require("./bff-http-error");
|
|
7
|
+
const TYPE_INVALID_REQUEST = 'invalid_request';
|
|
8
|
+
const TYPE_VALIDATION_ERROR = 'validation_error';
|
|
9
|
+
const TYPE_UNAUTHORIZED = 'unauthorized';
|
|
10
|
+
const TYPE_FORBIDDEN = 'forbidden';
|
|
11
|
+
const TYPE_NOT_FOUND = 'not_found';
|
|
12
|
+
const TYPE_UNPROCESSABLE = 'unprocessable_entity';
|
|
13
|
+
const TYPE_TOO_MANY_REQUESTS = 'too_many_requests';
|
|
14
|
+
const TYPE_NETWORK_ERROR = 'network_error';
|
|
15
|
+
const TYPE_AGENT_UNAVAILABLE = 'agent_unavailable';
|
|
16
|
+
const DEFAULT_NETWORK_MESSAGE = 'The agent could not be reached';
|
|
17
|
+
const DEFAULT_UNAVAILABLE_MESSAGE = 'The agent is unavailable';
|
|
18
|
+
const DEFAULT_ERROR_MESSAGE = 'Unexpected error';
|
|
19
|
+
exports.AGENT_ERROR_TYPE_MAP = {
|
|
20
|
+
ValidationError: TYPE_VALIDATION_ERROR,
|
|
21
|
+
BadRequestError: TYPE_INVALID_REQUEST,
|
|
22
|
+
UnauthorizedError: TYPE_UNAUTHORIZED,
|
|
23
|
+
ForbiddenError: TYPE_FORBIDDEN,
|
|
24
|
+
NotFoundError: TYPE_NOT_FOUND,
|
|
25
|
+
UnprocessableError: TYPE_UNPROCESSABLE,
|
|
26
|
+
TooManyRequestsError: TYPE_TOO_MANY_REQUESTS,
|
|
27
|
+
};
|
|
28
|
+
const FALLBACK_TYPE_BY_STATUS = {
|
|
29
|
+
400: TYPE_INVALID_REQUEST,
|
|
30
|
+
401: TYPE_UNAUTHORIZED,
|
|
31
|
+
403: TYPE_FORBIDDEN,
|
|
32
|
+
404: TYPE_NOT_FOUND,
|
|
33
|
+
422: TYPE_UNPROCESSABLE,
|
|
34
|
+
429: TYPE_TOO_MANY_REQUESTS,
|
|
35
|
+
};
|
|
36
|
+
function coerceStatus(status, fallback) {
|
|
37
|
+
const parsed = typeof status === 'string' ? Number(status) : status;
|
|
38
|
+
return typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : fallback;
|
|
39
|
+
}
|
|
40
|
+
function fallbackTypeByStatus(status) {
|
|
41
|
+
return FALLBACK_TYPE_BY_STATUS[status] ?? TYPE_INVALID_REQUEST;
|
|
42
|
+
}
|
|
43
|
+
function agentUnavailable() {
|
|
44
|
+
return new bff_http_error_1.BffHttpError(503, TYPE_AGENT_UNAVAILABLE, DEFAULT_UNAVAILABLE_MESSAGE);
|
|
45
|
+
}
|
|
46
|
+
function firstJsonApiError(body) {
|
|
47
|
+
if (typeof body !== 'object' || body === null)
|
|
48
|
+
return undefined;
|
|
49
|
+
const { errors } = body;
|
|
50
|
+
return Array.isArray(errors) ? errors[0] : undefined;
|
|
51
|
+
}
|
|
52
|
+
function mapJsonApiError(agentError, fallbackStatus, logger) {
|
|
53
|
+
const bodyStatus = coerceStatus(agentError.status, fallbackStatus);
|
|
54
|
+
// Never surface an upstream failure as a 2xx/3xx: ignore a non-error body status.
|
|
55
|
+
const status = bodyStatus >= 400 ? bodyStatus : fallbackStatus;
|
|
56
|
+
// The wrapped-message path skips the outer AgentHttpError 5xx check, so normalize here too.
|
|
57
|
+
if (status >= 500)
|
|
58
|
+
return agentUnavailable();
|
|
59
|
+
const message = agentError.detail ?? agentError.message ?? DEFAULT_ERROR_MESSAGE;
|
|
60
|
+
const mappedType = agentError.name ? exports.AGENT_ERROR_TYPE_MAP[agentError.name] : undefined;
|
|
61
|
+
if (agentError.name && mappedType === undefined) {
|
|
62
|
+
logger('Warn', 'Unmapped agent error name; using status-derived type', {
|
|
63
|
+
name: agentError.name,
|
|
64
|
+
status,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return new bff_http_error_1.BffHttpError(status, mappedType ?? fallbackTypeByStatus(status), message, agentError.data);
|
|
68
|
+
}
|
|
69
|
+
function parseJsonApiFromMessage(error) {
|
|
70
|
+
if (!(error instanceof Error))
|
|
71
|
+
return undefined;
|
|
72
|
+
try {
|
|
73
|
+
return firstJsonApiError(JSON.parse(error.message));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function mapFlatBody(status, body, responseText) {
|
|
80
|
+
const flat = (typeof body === 'object' && body !== null ? body : {});
|
|
81
|
+
const message = (typeof flat.error === 'string' ? flat.error : undefined) ??
|
|
82
|
+
(typeof flat.message === 'string' ? flat.message : undefined) ??
|
|
83
|
+
(typeof responseText === 'string' && responseText !== '' ? responseText : undefined) ??
|
|
84
|
+
DEFAULT_ERROR_MESSAGE;
|
|
85
|
+
return new bff_http_error_1.BffHttpError(status, fallbackTypeByStatus(status), message);
|
|
86
|
+
}
|
|
87
|
+
function mapAgentError(error, { logger }) {
|
|
88
|
+
// A BFF-origin error that reached here (e.g. thrown inside a caller's agent-call try/catch) keeps
|
|
89
|
+
// its own type — it must not be recategorized as a transport/agent error.
|
|
90
|
+
if (error instanceof bff_http_error_1.BffHttpError)
|
|
91
|
+
return error;
|
|
92
|
+
if (!(error instanceof agent_client_1.AgentHttpError)) {
|
|
93
|
+
const agentError = parseJsonApiFromMessage(error);
|
|
94
|
+
if (agentError)
|
|
95
|
+
return mapJsonApiError(agentError, 400, logger);
|
|
96
|
+
// No HTTP response: treated as a transport failure reaching the agent. Log the real cause but
|
|
97
|
+
// return a generic message — transport errors embed internal topology (hostnames/IPs) that must
|
|
98
|
+
// not leak to the client. Semantic agent-client errors (action form validation / approval
|
|
99
|
+
// outcomes) are NOT transport failures: the action endpoint catches and maps those before this
|
|
100
|
+
// fallback. Callers must scope their try/catch to the agent call so a local BFF bug surfaces as
|
|
101
|
+
// a 500 through the error middleware rather than being mislabelled here.
|
|
102
|
+
logger('Warn', 'Agent transport failure mapped to network_error', {
|
|
103
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
104
|
+
});
|
|
105
|
+
return new bff_http_error_1.BffHttpError(502, TYPE_NETWORK_ERROR, DEFAULT_NETWORK_MESSAGE);
|
|
106
|
+
}
|
|
107
|
+
if (error.status >= 500)
|
|
108
|
+
return agentUnavailable();
|
|
109
|
+
const agentError = firstJsonApiError(error.body);
|
|
110
|
+
if (agentError)
|
|
111
|
+
return mapJsonApiError(agentError, error.status, logger);
|
|
112
|
+
return mapFlatBody(error.status, error.body, error.responseText);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWdlbnQtZXJyb3ItbWFwcGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2h0dHAvYWdlbnQtZXJyb3ItbWFwcGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQXNJQSxzQ0E0QkM7QUFoS0QsNERBQTJEO0FBRTNELHFEQUFnRDtBQUVoRCxNQUFNLG9CQUFvQixHQUFHLGlCQUFpQixDQUFDO0FBQy9DLE1BQU0scUJBQXFCLEdBQUcsa0JBQWtCLENBQUM7QUFDakQsTUFBTSxpQkFBaUIsR0FBRyxjQUFjLENBQUM7QUFDekMsTUFBTSxjQUFjLEdBQUcsV0FBVyxDQUFDO0FBQ25DLE1BQU0sY0FBYyxHQUFHLFdBQVcsQ0FBQztBQUNuQyxNQUFNLGtCQUFrQixHQUFHLHNCQUFzQixDQUFDO0FBQ2xELE1BQU0sc0JBQXNCLEdBQUcsbUJBQW1CLENBQUM7QUFDbkQsTUFBTSxrQkFBa0IsR0FBRyxlQUFlLENBQUM7QUFDM0MsTUFBTSxzQkFBc0IsR0FBRyxtQkFBbUIsQ0FBQztBQWFuRCxNQUFNLHVCQUF1QixHQUFHLGdDQUFnQyxDQUFDO0FBQ2pFLE1BQU0sMkJBQTJCLEdBQUcsMEJBQTBCLENBQUM7QUFDL0QsTUFBTSxxQkFBcUIsR0FBRyxrQkFBa0IsQ0FBQztBQUVwQyxRQUFBLG9CQUFvQixHQUFpQztJQUNoRSxlQUFlLEVBQUUscUJBQXFCO0lBQ3RDLGVBQWUsRUFBRSxvQkFBb0I7SUFDckMsaUJBQWlCLEVBQUUsaUJBQWlCO0lBQ3BDLGNBQWMsRUFBRSxjQUFjO0lBQzlCLGFBQWEsRUFBRSxjQUFjO0lBQzdCLGtCQUFrQixFQUFFLGtCQUFrQjtJQUN0QyxvQkFBb0IsRUFBRSxzQkFBc0I7Q0FDN0MsQ0FBQztBQUVGLE1BQU0sdUJBQXVCLEdBQWlDO0lBQzVELEdBQUcsRUFBRSxvQkFBb0I7SUFDekIsR0FBRyxFQUFFLGlCQUFpQjtJQUN0QixHQUFHLEVBQUUsY0FBYztJQUNuQixHQUFHLEVBQUUsY0FBYztJQUNuQixH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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BffHttpError } from './bff-http-error';
|
|
2
|
+
export declare function unknownCollection(message?: string): BffHttpError;
|
|
3
|
+
export declare function unknownRelation(message?: string): BffHttpError;
|
|
4
|
+
export declare function unknownAction(message?: string): BffHttpError;
|
|
5
|
+
export declare function collectionNotAllowed(message?: string): BffHttpError;
|
|
6
|
+
export declare function relationNotAllowed(message?: string): BffHttpError;
|
|
7
|
+
export declare function actionNotAllowed(message?: string): BffHttpError;
|
|
8
|
+
export declare function invalidRequest(message?: string, details?: unknown): BffHttpError;
|
|
9
|
+
export declare function mappingError(message?: string): BffHttpError;
|
|
10
|
+
export declare function schemaUnavailable(message?: string): BffHttpError;
|
|
11
|
+
export declare function unsupportedActionResult(message?: string): BffHttpError;
|
|
12
|
+
//# sourceMappingURL=bff-local-errors.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unknownCollection = unknownCollection;
|
|
4
|
+
exports.unknownRelation = unknownRelation;
|
|
5
|
+
exports.unknownAction = unknownAction;
|
|
6
|
+
exports.collectionNotAllowed = collectionNotAllowed;
|
|
7
|
+
exports.relationNotAllowed = relationNotAllowed;
|
|
8
|
+
exports.actionNotAllowed = actionNotAllowed;
|
|
9
|
+
exports.invalidRequest = invalidRequest;
|
|
10
|
+
exports.mappingError = mappingError;
|
|
11
|
+
exports.schemaUnavailable = schemaUnavailable;
|
|
12
|
+
exports.unsupportedActionResult = unsupportedActionResult;
|
|
13
|
+
const bff_http_error_1 = require("./bff-http-error");
|
|
14
|
+
function unknownCollection(message = 'Unknown collection') {
|
|
15
|
+
return new bff_http_error_1.BffHttpError(404, 'unknown_collection', message);
|
|
16
|
+
}
|
|
17
|
+
function unknownRelation(message = 'Unknown relation') {
|
|
18
|
+
return new bff_http_error_1.BffHttpError(404, 'unknown_relation', message);
|
|
19
|
+
}
|
|
20
|
+
function unknownAction(message = 'Unknown action') {
|
|
21
|
+
return new bff_http_error_1.BffHttpError(404, 'unknown_action', message);
|
|
22
|
+
}
|
|
23
|
+
function collectionNotAllowed(message = 'Collection is not allowed') {
|
|
24
|
+
return new bff_http_error_1.BffHttpError(403, 'collection_not_allowed', message);
|
|
25
|
+
}
|
|
26
|
+
function relationNotAllowed(message = 'Relation is not allowed') {
|
|
27
|
+
return new bff_http_error_1.BffHttpError(403, 'relation_not_allowed', message);
|
|
28
|
+
}
|
|
29
|
+
function actionNotAllowed(message = 'Action is not allowed') {
|
|
30
|
+
return new bff_http_error_1.BffHttpError(403, 'action_not_allowed', message);
|
|
31
|
+
}
|
|
32
|
+
function invalidRequest(message = 'Invalid request', details) {
|
|
33
|
+
return new bff_http_error_1.BffHttpError(400, 'invalid_request', message, details);
|
|
34
|
+
}
|
|
35
|
+
function mappingError(message = 'Failed to map the agent response') {
|
|
36
|
+
return new bff_http_error_1.BffHttpError(500, 'mapping_error', message);
|
|
37
|
+
}
|
|
38
|
+
function schemaUnavailable(message = 'The agent schema is unavailable') {
|
|
39
|
+
return new bff_http_error_1.BffHttpError(503, 'schema_unavailable', message);
|
|
40
|
+
}
|
|
41
|
+
function unsupportedActionResult(message = 'Unsupported action result') {
|
|
42
|
+
return new bff_http_error_1.BffHttpError(501, 'unsupported_action_result', message);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmZmLWxvY2FsLWVycm9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9odHRwL2JmZi1sb2NhbC1lcnJvcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFFQSw4Q0FFQztBQUVELDBDQUVDO0FBRUQsc0NBRUM7QUFFRCxvREFFQztBQUVELGdEQUVDO0FBRUQsNENBRUM7QUFFRCx3Q0FFQztBQUVELG9DQUVDO0FBRUQsOENBRUM7QUFFRCwwREFFQztBQXhDRCxxREFBZ0Q7QUFFaEQsU0FBZ0IsaUJBQWlCLENBQUMsT0FBTyxHQUFHLG9CQUFvQjtJQUM5RCxPQUFPLElBQUksNkJBQVksQ0FBQyxHQUFHLEVBQUUsb0JBQW9CLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDOUQsQ0FBQztBQUVELFNBQWdCLGVBQWUsQ0FBQyxPQUFPLEdBQUcsa0JBQWtCO0lBQzFELE9BQU8sSUFBSSw2QkFBWSxDQUFDLEdBQUcsRUFBRSxrQkFBa0IsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUM1RCxDQUFDO0FBRUQsU0FBZ0IsYUFBYSxDQUFDLE9BQU8sR0FBRyxnQkFBZ0I7SUFDdEQsT0FBTyxJQUFJLDZCQUFZLENBQUMsR0FBRyxFQUFFLGdCQUFnQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQzFELENBQUM7QUFFRCxTQUFnQixvQkFBb0IsQ0FBQyxPQUFPLEdBQUcsMkJBQTJCO0lBQ3hFLE9BQU8sSUFBSSw2QkFBWSxDQUFDLEdBQUcsRUFBRSx3QkFBd0IsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUNsRSxDQUFDO0FBRUQsU0FBZ0Isa0JBQWtCLENBQUMsT0FBTyxHQUFHLHlCQUF5QjtJQUNwRSxPQUFPLElBQUksNkJBQVksQ0FBQyxHQUFHLEVBQUUsc0JBQXNCLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDaEUsQ0FBQztBQUVELFNBQWdCLGdCQUFnQixDQUFDLE9BQU8sR0FBRyx1QkFBdUI7SUFDaEUsT0FBTyxJQUFJLDZCQUFZLENBQUMsR0FBRyxFQUFFLG9CQUFvQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQzlELENBQUM7QUFFRCxTQUFnQixjQUFjLENBQUMsT0FBTyxHQUFHLGlCQUFpQixFQUFFLE9BQWlCO0lBQzNFLE9BQU8sSUFBSSw2QkFBWSxDQUFDLEdBQUcsRUFBRSxpQkFBaUIsRUFBRSxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDcEUsQ0FBQztBQUVELFNBQWdCLFlBQVksQ0FBQyxPQUFPLEdBQUcsa0NBQWtDO0lBQ3ZFLE9BQU8sSUFBSSw2QkFBWSxDQUFDLEdBQUcsRUFBRSxlQUFlLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDekQsQ0FBQztBQUVELFNBQWdCLGlCQUFpQixDQUFDLE9BQU8sR0FBRyxpQ0FBaUM7SUFDM0UsT0FBTyxJQUFJLDZCQUFZLENBQUMsR0FBRyxFQUFFLG9CQUFvQixFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQzlELENBQUM7QUFFRCxTQUFnQix1QkFBdUIsQ0FBQyxPQUFPLEdBQUcsMkJBQTJCO0lBQzNFLE9BQU8sSUFBSSw2QkFBWSxDQUFDLEdBQUcsRUFBRSwyQkFBMkIsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUNyRSxDQUFDIn0=
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy1wb3J0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3BvcnRzL21ldHJpY3MtcG9ydC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type ReadModel from './read-model';
|
|
2
|
+
import type { Metrics } from '../ports/metrics-port';
|
|
3
|
+
import type { ActionEndpointsByCollection } from '@forestadmin/agent-client';
|
|
4
|
+
export declare const ACTION_ENDPOINT_MISS = "action_endpoint_miss";
|
|
5
|
+
export declare const ACTION_ENDPOINT_ERROR = "action_endpoint_error";
|
|
6
|
+
export type ActionEndpointInfo = ActionEndpointsByCollection[string][string];
|
|
7
|
+
export type ReadModelProvider = () => Promise<ReadModel>;
|
|
8
|
+
export interface ResolveActionContext {
|
|
9
|
+
rendering: string | number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves an action to its endpoint against the current read-model. Never throws: an absent
|
|
13
|
+
* mapping emits the miss counter, a failure to obtain the read-model emits the error counter.
|
|
14
|
+
* Both counters carry `rendering`, `collection`, and `action` tags. In normal flow the action
|
|
15
|
+
* allow-list already excludes endpoint-less actions, so the miss path is a defensive guard.
|
|
16
|
+
*/
|
|
17
|
+
export default class ActionEndpointResolver {
|
|
18
|
+
private readonly getReadModel;
|
|
19
|
+
private readonly metrics;
|
|
20
|
+
constructor(getReadModel: ReadModelProvider, metrics: Metrics);
|
|
21
|
+
resolve(collection: string, action: string, { rendering }: ResolveActionContext): Promise<ActionEndpointInfo | undefined>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=action-endpoint-resolver.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ACTION_ENDPOINT_ERROR = exports.ACTION_ENDPOINT_MISS = void 0;
|
|
4
|
+
exports.ACTION_ENDPOINT_MISS = 'action_endpoint_miss';
|
|
5
|
+
exports.ACTION_ENDPOINT_ERROR = 'action_endpoint_error';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves an action to its endpoint against the current read-model. Never throws: an absent
|
|
8
|
+
* mapping emits the miss counter, a failure to obtain the read-model emits the error counter.
|
|
9
|
+
* Both counters carry `rendering`, `collection`, and `action` tags. In normal flow the action
|
|
10
|
+
* allow-list already excludes endpoint-less actions, so the miss path is a defensive guard.
|
|
11
|
+
*/
|
|
12
|
+
class ActionEndpointResolver {
|
|
13
|
+
constructor(getReadModel, metrics) {
|
|
14
|
+
this.getReadModel = getReadModel;
|
|
15
|
+
this.metrics = metrics;
|
|
16
|
+
}
|
|
17
|
+
async resolve(collection, action, { rendering }) {
|
|
18
|
+
const tags = { rendering, collection, action };
|
|
19
|
+
let readModel;
|
|
20
|
+
try {
|
|
21
|
+
readModel = await this.getReadModel();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
this.metrics.increment(exports.ACTION_ENDPOINT_ERROR, tags);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const info = readModel.getActionEndpoints()[collection]?.[action];
|
|
28
|
+
if (!info) {
|
|
29
|
+
this.metrics.increment(exports.ACTION_ENDPOINT_MISS, tags);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return info;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.default = ActionEndpointResolver;
|
|
36
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aW9uLWVuZHBvaW50LXJlc29sdmVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlYWQtbW9kZWwvYWN0aW9uLWVuZHBvaW50LXJlc29sdmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUlhLFFBQUEsb0JBQW9CLEdBQUcsc0JBQXNCLENBQUM7QUFDOUMsUUFBQSxxQkFBcUIsR0FBRyx1QkFBdUIsQ0FBQztBQVU3RDs7Ozs7R0FLRztBQUNILE1BQXFCLHNCQUFzQjtJQUN6QyxZQUNtQixZQUErQixFQUMvQixPQUFnQjtRQURoQixpQkFBWSxHQUFaLFlBQVksQ0FBbUI7UUFDL0IsWUFBTyxHQUFQLE9BQU8sQ0FBUztJQUNoQyxDQUFDO0lBRUosS0FBSyxDQUFDLE9BQU8sQ0FDWCxVQUFrQixFQUNsQixNQUFjLEVBQ2QsRUFBRSxTQUFTLEVBQXdCO1FBRW5DLE1BQU0sSUFBSSxHQUFHLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsQ0FBQztRQUUvQyxJQUFJLFNBQW9CLENBQUM7UUFFekIsSUFBSSxDQUFDO1lBQ0gsU0FBUyxHQUFHLE1BQU0sSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQ3hDLENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxJQUFJLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyw2QkFBcUIsRUFBRSxJQUFJLENBQUMsQ0FBQztZQUVwRCxPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBRUQsTUFBTSxJQUFJLEdBQUcsU0FBUyxDQUFDLGtCQUFrQixFQUFFLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUVsRSxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDVixJQUFJLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyw0QkFBb0IsRUFBRSxJQUFJLENBQUMsQ0FBQztZQUVuRCxPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0NBQ0Y7QUFqQ0QseUNBaUNDIn0=
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CapabilitiesFetcher } from './capabilities-cache';
|
|
2
|
+
export interface AgentCapabilitiesFetcherOptions {
|
|
3
|
+
agentUrl: string;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Builds a capabilities fetcher bound to a request's agent token. The cache calls it only on a
|
|
8
|
+
* miss, so the token of whichever request first populates a collection is the one used.
|
|
9
|
+
*/
|
|
10
|
+
export default function createAgentCapabilitiesFetcher({ agentUrl, token, }: AgentCapabilitiesFetcherOptions): CapabilitiesFetcher;
|
|
11
|
+
//# sourceMappingURL=agent-capabilities-fetcher.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = createAgentCapabilitiesFetcher;
|
|
4
|
+
const agent_client_1 = require("@forestadmin/agent-client");
|
|
5
|
+
/**
|
|
6
|
+
* Builds a capabilities fetcher bound to a request's agent token. The cache calls it only on a
|
|
7
|
+
* miss, so the token of whichever request first populates a collection is the one used.
|
|
8
|
+
*/
|
|
9
|
+
function createAgentCapabilitiesFetcher({ agentUrl, token, }) {
|
|
10
|
+
const client = (0, agent_client_1.createRemoteAgentClient)({ url: agentUrl, token });
|
|
11
|
+
return collection => client.collection(collection).capabilities();
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWdlbnQtY2FwYWJpbGl0aWVzLWZldGNoZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcmVhZC1tb2RlbC9hZ2VudC1jYXBhYmlsaXRpZXMtZmV0Y2hlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQWFBLGlEQU9DO0FBbEJELDREQUFvRTtBQU9wRTs7O0dBR0c7QUFDSCxTQUF3Qiw4QkFBOEIsQ0FBQyxFQUNyRCxRQUFRLEVBQ1IsS0FBSyxHQUMyQjtJQUNoQyxNQUFNLE1BQU0sR0FBRyxJQUFBLHNDQUF1QixFQUFDLEVBQUUsR0FBRyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBRWpFLE9BQU8sVUFBVSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFVBQVUsQ0FBQyxDQUFDLFlBQVksRUFBRSxDQUFDO0FBQ3BFLENBQUMifQ==
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface CapabilitiesResult {
|
|
2
|
+
fields: {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
operators: string[];
|
|
6
|
+
}[];
|
|
7
|
+
}
|
|
8
|
+
export type CapabilitiesFetcher = (collection: string) => Promise<CapabilitiesResult>;
|
|
9
|
+
export interface CapabilitiesCacheOptions {
|
|
10
|
+
now?: () => number;
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Per-collection capabilities cache (24h). The fetcher is passed per call so it can be bound to
|
|
15
|
+
* the caller's request token. Invalidated together with the schema via `clear()`.
|
|
16
|
+
*/
|
|
17
|
+
export default class CapabilitiesCache {
|
|
18
|
+
private readonly now;
|
|
19
|
+
private readonly ttlMs;
|
|
20
|
+
private readonly entries;
|
|
21
|
+
private readonly inFlight;
|
|
22
|
+
private generation;
|
|
23
|
+
constructor({ now, ttlMs }?: CapabilitiesCacheOptions);
|
|
24
|
+
get(collection: string, fetcher: CapabilitiesFetcher): Promise<CapabilitiesResult>;
|
|
25
|
+
clear(): void;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=capabilities-cache.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const schema_cache_1 = require("./schema-cache");
|
|
4
|
+
/**
|
|
5
|
+
* Per-collection capabilities cache (24h). The fetcher is passed per call so it can be bound to
|
|
6
|
+
* the caller's request token. Invalidated together with the schema via `clear()`.
|
|
7
|
+
*/
|
|
8
|
+
class CapabilitiesCache {
|
|
9
|
+
constructor({ now = Date.now, ttlMs = schema_cache_1.ONE_DAY_MS } = {}) {
|
|
10
|
+
this.entries = new Map();
|
|
11
|
+
this.inFlight = new Map();
|
|
12
|
+
this.generation = 0;
|
|
13
|
+
this.now = now;
|
|
14
|
+
this.ttlMs = ttlMs;
|
|
15
|
+
}
|
|
16
|
+
async get(collection, fetcher) {
|
|
17
|
+
const entry = this.entries.get(collection);
|
|
18
|
+
if (entry && this.now() - entry.fetchedAt < this.ttlMs)
|
|
19
|
+
return entry.result;
|
|
20
|
+
const pending = this.inFlight.get(collection);
|
|
21
|
+
if (pending)
|
|
22
|
+
return pending;
|
|
23
|
+
const { generation } = this;
|
|
24
|
+
const fetch = fetcher(collection)
|
|
25
|
+
.then(result => {
|
|
26
|
+
// Skip the write if a clear() (schema refresh) happened while this fetch was in flight —
|
|
27
|
+
// its result belongs to the old schema generation and must not repopulate the cache.
|
|
28
|
+
if (this.generation === generation) {
|
|
29
|
+
this.entries.set(collection, { result, fetchedAt: this.now() });
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
})
|
|
33
|
+
.finally(() => {
|
|
34
|
+
if (this.inFlight.get(collection) === fetch)
|
|
35
|
+
this.inFlight.delete(collection);
|
|
36
|
+
});
|
|
37
|
+
this.inFlight.set(collection, fetch);
|
|
38
|
+
return fetch;
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
this.generation += 1;
|
|
42
|
+
this.entries.clear();
|
|
43
|
+
this.inFlight.clear();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.default = CapabilitiesCache;
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FwYWJpbGl0aWVzLWNhY2hlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlYWQtbW9kZWwvY2FwYWJpbGl0aWVzLWNhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsaURBQTRDO0FBa0I1Qzs7O0dBR0c7QUFDSCxNQUFxQixpQkFBaUI7SUFRcEMsWUFBWSxFQUFFLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssR0FBRyx5QkFBVSxLQUErQixFQUFFO1FBSmhFLFlBQU8sR0FBRyxJQUFJLEdBQUcsRUFBc0IsQ0FBQztRQUN4QyxhQUFRLEdBQUcsSUFBSSxHQUFHLEVBQXVDLENBQUM7UUFDbkUsZUFBVSxHQUFHLENBQUMsQ0FBQztRQUdyQixJQUFJLENBQUMsR0FBRyxHQUFHLEdBQUcsQ0FBQztRQUNmLElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDO0lBQ3JCLENBQUM7SUFFRCxLQUFLLENBQUMsR0FBRyxDQUFDLFVBQWtCLEVBQUUsT0FBNEI7UUFDeEQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0MsSUFBSSxLQUFLLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLEtBQUssQ0FBQyxTQUFTLEdBQUcsSUFBSSxDQUFDLEtBQUs7WUFBRSxPQUFPLEtBQUssQ0FBQyxNQUFNLENBQUM7UUFFNUUsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDOUMsSUFBSSxPQUFPO1lBQUUsT0FBTyxPQUFPLENBQUM7UUFFNUIsTUFBTSxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksQ0FBQztRQUM1QixNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDO2FBQzlCLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRTtZQUNiLHlGQUF5RjtZQUN6RixxRkFBcUY7WUFDckYsSUFBSSxJQUFJLENBQUMsVUFBVSxLQUFLLFVBQVUsRUFBRSxDQUFDO2dCQUNuQyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDbEUsQ0FBQztZQUVELE9BQU8sTUFBTSxDQUFDO1FBQ2hCLENBQUMsQ0FBQzthQUNELE9BQU8sQ0FBQyxHQUFHLEVBQUU7WUFDWixJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxLQUFLLEtBQUs7Z0JBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDaEYsQ0FBQyxDQUFDLENBQUM7UUFFTCxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFFckMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRUQsS0FBSztRQUNILElBQUksQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDO1FBQ3JCLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDckIsSUFBSSxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUN4QixDQUFDO0NBQ0Y7QUE3Q0Qsb0NBNkNDIn0=
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Logger } from '../ports/logger-port';
|
|
2
|
+
import type { Metrics } from '../ports/metrics-port';
|
|
3
|
+
import ActionEndpointResolver from './action-endpoint-resolver';
|
|
4
|
+
import ReadModelStore from './read-model-store';
|
|
5
|
+
export interface CreateReadModelOptions {
|
|
6
|
+
forestServerUrl: string;
|
|
7
|
+
envSecret: string;
|
|
8
|
+
metrics?: Metrics;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
export interface ReadModelBundle {
|
|
13
|
+
store: ReadModelStore;
|
|
14
|
+
actionEndpointResolver: ActionEndpointResolver;
|
|
15
|
+
}
|
|
16
|
+
export default function createReadModel({ forestServerUrl, envSecret, metrics, logger, now, }: CreateReadModelOptions): ReadModelBundle;
|
|
17
|
+
//# sourceMappingURL=create-read-model.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = createReadModel;
|
|
7
|
+
const action_endpoint_resolver_1 = __importDefault(require("./action-endpoint-resolver"));
|
|
8
|
+
const capabilities_cache_1 = __importDefault(require("./capabilities-cache"));
|
|
9
|
+
const forest_schema_client_1 = __importDefault(require("./forest-schema-client"));
|
|
10
|
+
const read_model_store_1 = __importDefault(require("./read-model-store"));
|
|
11
|
+
const schema_cache_1 = __importDefault(require("./schema-cache"));
|
|
12
|
+
const console_metrics_1 = __importDefault(require("../adapters/console-metrics"));
|
|
13
|
+
const safe_metrics_1 = __importDefault(require("../adapters/safe-metrics"));
|
|
14
|
+
function createReadModel({ forestServerUrl, envSecret, metrics, logger, now, }) {
|
|
15
|
+
// Wrap once so a throwing metrics backend can never break business logic anywhere in the bundle.
|
|
16
|
+
const resolvedMetrics = (0, safe_metrics_1.default)(metrics ?? (0, console_metrics_1.default)(logger));
|
|
17
|
+
const fetcher = new forest_schema_client_1.default({ forestServerUrl, envSecret });
|
|
18
|
+
const schemaCache = new schema_cache_1.default({ fetcher, metrics: resolvedMetrics, now });
|
|
19
|
+
const capabilitiesCache = new capabilities_cache_1.default({ now });
|
|
20
|
+
const store = new read_model_store_1.default(schemaCache, capabilitiesCache);
|
|
21
|
+
const actionEndpointResolver = new action_endpoint_resolver_1.default(() => store.getReadModel(), resolvedMetrics);
|
|
22
|
+
return { store, actionEndpointResolver };
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLXJlYWQtbW9kZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcmVhZC1tb2RlbC9jcmVhdGUtcmVhZC1tb2RlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQXdCQSxrQ0FtQkM7QUF4Q0QsMEZBQWdFO0FBQ2hFLDhFQUFxRDtBQUNyRCxrRkFBd0Q7QUFDeEQsMEVBQWdEO0FBQ2hELGtFQUF5QztBQUN6QyxrRkFBK0Q7QUFDL0QsNEVBQW1EO0FBZW5ELFNBQXdCLGVBQWUsQ0FBQyxFQUN0QyxlQUFlLEVBQ2YsU0FBUyxFQUNULE9BQU8sRUFDUCxNQUFNLEVBQ04sR0FBRyxHQUNvQjtJQUN2QixpR0FBaUc7SUFDakcsTUFBTSxlQUFlLEdBQUcsSUFBQSxzQkFBVyxFQUFDLE9BQU8sSUFBSSxJQUFBLHlCQUFvQixFQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFDN0UsTUFBTSxPQUFPLEdBQUcsSUFBSSw4QkFBa0IsQ0FBQyxFQUFFLGVBQWUsRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDO0lBQ3ZFLE1BQU0sV0FBVyxHQUFHLElBQUksc0JBQVcsQ0FBQyxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsZUFBZSxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDaEYsTUFBTSxpQkFBaUIsR0FBRyxJQUFJLDRCQUFpQixDQUFDLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztJQUN6RCxNQUFNLEtBQUssR0FBRyxJQUFJLDBCQUFjLENBQUMsV0FBVyxFQUFFLGlCQUFpQixDQUFDLENBQUM7SUFDakUsTUFBTSxzQkFBc0IsR0FBRyxJQUFJLGtDQUFzQixDQUN2RCxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsWUFBWSxFQUFFLEVBQzFCLGVBQWUsQ0FDaEIsQ0FBQztJQUVGLE9BQU8sRUFBRSxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsQ0FBQztBQUMzQyxDQUFDIn0=
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class SchemaUnavailableError extends Error {
|
|
4
|
+
constructor(cause) {
|
|
5
|
+
super('The agent schema is unavailable');
|
|
6
|
+
this.name = 'SchemaUnavailableError';
|
|
7
|
+
this.cause = cause;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.default = SchemaUnavailableError;
|
|
11
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlYWQtbW9kZWwvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsTUFBcUIsc0JBQXVCLFNBQVEsS0FBSztJQUd2RCxZQUFZLEtBQWU7UUFDekIsS0FBSyxDQUFDLGlDQUFpQyxDQUFDLENBQUM7UUFDekMsSUFBSSxDQUFDLElBQUksR0FBRyx3QkFBd0IsQ0FBQztRQUNyQyxJQUFJLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQztJQUNyQixDQUFDO0NBQ0Y7QUFSRCx5Q0FRQyJ9
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ForestSchemaCollection } from '@forestadmin/forestadmin-client';
|
|
2
|
+
export interface ForestSchemaClientOptions {
|
|
3
|
+
forestServerUrl: string;
|
|
4
|
+
envSecret: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SchemaFetcher {
|
|
7
|
+
fetchSchema(): Promise<ForestSchemaCollection[]>;
|
|
8
|
+
}
|
|
9
|
+
export default class ForestSchemaClient implements SchemaFetcher {
|
|
10
|
+
private readonly schemaService;
|
|
11
|
+
constructor({ forestServerUrl, envSecret }: ForestSchemaClientOptions);
|
|
12
|
+
fetchSchema(): Promise<ForestSchemaCollection[]>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=forest-schema-client.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
|
|
4
|
+
class ForestSchemaClient {
|
|
5
|
+
constructor({ forestServerUrl, envSecret }) {
|
|
6
|
+
this.schemaService = new forestadmin_client_1.SchemaService(new forestadmin_client_1.ForestHttpApi(), { forestServerUrl, envSecret });
|
|
7
|
+
}
|
|
8
|
+
async fetchSchema() {
|
|
9
|
+
return this.schemaService.getSchema();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.default = ForestSchemaClient;
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9yZXN0LXNjaGVtYS1jbGllbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcmVhZC1tb2RlbC9mb3Jlc3Qtc2NoZW1hLWNsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUVBLHdFQUErRTtBQVcvRSxNQUFxQixrQkFBa0I7SUFHckMsWUFBWSxFQUFFLGVBQWUsRUFBRSxTQUFTLEVBQTZCO1FBQ25FLElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxrQ0FBYSxDQUFDLElBQUksa0NBQWEsRUFBRSxFQUFFLEVBQUUsZUFBZSxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUM7SUFDOUYsQ0FBQztJQUVELEtBQUssQ0FBQyxXQUFXO1FBQ2YsT0FBTyxJQUFJLENBQUMsYUFBYSxDQUFDLFNBQVMsRUFBRSxDQUFDO0lBQ3hDLENBQUM7Q0FDRjtBQVZELHFDQVVDIn0=
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type CapabilitiesCache from './capabilities-cache';
|
|
2
|
+
import type { CapabilitiesFetcher, CapabilitiesResult } from './capabilities-cache';
|
|
3
|
+
import type SchemaCache from './schema-cache';
|
|
4
|
+
import ReadModel from './read-model';
|
|
5
|
+
/**
|
|
6
|
+
* Single owner of the coupled schema + capabilities lifecycle. A successful schema refresh (a bump
|
|
7
|
+
* of the cache `revision`) rebuilds the read-model and clears capabilities atomically, so the
|
|
8
|
+
* allow-list and capabilities never split-brain across schema generations.
|
|
9
|
+
*/
|
|
10
|
+
export default class ReadModelStore {
|
|
11
|
+
private readonly schemaCache;
|
|
12
|
+
private readonly capabilitiesCache;
|
|
13
|
+
private builtRevision;
|
|
14
|
+
private readModel;
|
|
15
|
+
constructor(schemaCache: SchemaCache, capabilitiesCache: CapabilitiesCache);
|
|
16
|
+
getReadModel(): Promise<ReadModel>;
|
|
17
|
+
getCapabilities(collection: string, fetcher: CapabilitiesFetcher): Promise<CapabilitiesResult>;
|
|
18
|
+
ageSeconds(): number | undefined;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=read-model-store.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const read_model_1 = __importDefault(require("./read-model"));
|
|
7
|
+
/**
|
|
8
|
+
* Single owner of the coupled schema + capabilities lifecycle. A successful schema refresh (a bump
|
|
9
|
+
* of the cache `revision`) rebuilds the read-model and clears capabilities atomically, so the
|
|
10
|
+
* allow-list and capabilities never split-brain across schema generations.
|
|
11
|
+
*/
|
|
12
|
+
class ReadModelStore {
|
|
13
|
+
constructor(schemaCache, capabilitiesCache) {
|
|
14
|
+
this.builtRevision = -1;
|
|
15
|
+
this.readModel = null;
|
|
16
|
+
this.schemaCache = schemaCache;
|
|
17
|
+
this.capabilitiesCache = capabilitiesCache;
|
|
18
|
+
}
|
|
19
|
+
async getReadModel() {
|
|
20
|
+
const collections = await this.schemaCache.get();
|
|
21
|
+
if (this.schemaCache.revision !== this.builtRevision || !this.readModel) {
|
|
22
|
+
this.readModel = new read_model_1.default(collections);
|
|
23
|
+
this.builtRevision = this.schemaCache.revision;
|
|
24
|
+
this.capabilitiesCache.clear();
|
|
25
|
+
}
|
|
26
|
+
return this.readModel;
|
|
27
|
+
}
|
|
28
|
+
async getCapabilities(collection, fetcher) {
|
|
29
|
+
// Ensure any pending schema refresh (and its capabilities invalidation) runs first.
|
|
30
|
+
await this.getReadModel();
|
|
31
|
+
// TODO(wiring): possible TOCTOU once this is called from request handling. A concurrent schema
|
|
32
|
+
// refresh can clear capabilities while this fetch is in flight, so the caller could receive
|
|
33
|
+
// capabilities from the previous schema generation alongside the new allow-list. When wiring
|
|
34
|
+
// the data endpoints, re-check `schemaCache.revision` after the fetch resolves and retry on a
|
|
35
|
+
// mismatch so capabilities and schema stay atomically coupled.
|
|
36
|
+
return this.capabilitiesCache.get(collection, fetcher);
|
|
37
|
+
}
|
|
38
|
+
ageSeconds() {
|
|
39
|
+
return this.schemaCache.ageSeconds();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.default = ReadModelStore;
|
|
43
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVhZC1tb2RlbC1zdG9yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9yZWFkLW1vZGVsL3JlYWQtbW9kZWwtc3RvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFJQSw4REFBcUM7QUFFckM7Ozs7R0FJRztBQUNILE1BQXFCLGNBQWM7SUFPakMsWUFBWSxXQUF3QixFQUFFLGlCQUFvQztRQUhsRSxrQkFBYSxHQUFHLENBQUMsQ0FBQyxDQUFDO1FBQ25CLGNBQVMsR0FBcUIsSUFBSSxDQUFDO1FBR3pDLElBQUksQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO1FBQy9CLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxpQkFBaUIsQ0FBQztJQUM3QyxDQUFDO0lBRUQsS0FBSyxDQUFDLFlBQVk7UUFDaEIsTUFBTSxXQUFXLEdBQUcsTUFBTSxJQUFJLENBQUMsV0FBVyxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBRWpELElBQUksSUFBSSxDQUFDLFdBQVcsQ0FBQyxRQUFRLEtBQUssSUFBSSxDQUFDLGFBQWEsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsQ0FBQztZQUN4RSxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksb0JBQVMsQ0FBQyxXQUFXLENBQUMsQ0FBQztZQUM1QyxJQUFJLENBQUMsYUFBYSxHQUFHLElBQUksQ0FBQyxXQUFXLENBQUMsUUFBUSxDQUFDO1lBQy9DLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNqQyxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUMsU0FBUyxDQUFDO0lBQ3hCLENBQUM7SUFFRCxLQUFLLENBQUMsZUFBZSxDQUNuQixVQUFrQixFQUNsQixPQUE0QjtRQUU1QixvRkFBb0Y7UUFDcEYsTUFBTSxJQUFJLENBQUMsWUFBWSxFQUFFLENBQUM7UUFFMUIsK0ZBQStGO1FBQy9GLDRGQUE0RjtRQUM1Riw2RkFBNkY7UUFDN0YsOEZBQThGO1FBQzlGLCtEQUErRDtRQUMvRCxPQUFPLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLENBQUMsVUFBVSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3pELENBQUM7SUFFRCxVQUFVO1FBQ1IsT0FBTyxJQUFJLENBQUMsV0FBVyxDQUFDLFVBQVUsRUFBRSxDQUFDO0lBQ3ZDLENBQUM7Q0FDRjtBQTFDRCxpQ0EwQ0MifQ==
|