@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,39 @@
|
|
|
1
|
+
import type { ActionEndpointsByCollection } from '@forestadmin/agent-client';
|
|
2
|
+
import type { ForestSchemaCollection } from '@forestadmin/forestadmin-client';
|
|
3
|
+
export type RelationshipType = 'BelongsTo' | 'HasOne' | 'HasMany' | 'BelongsToMany';
|
|
4
|
+
export type RelationTarget = {
|
|
5
|
+
type: RelationshipType;
|
|
6
|
+
polymorphic: false;
|
|
7
|
+
target: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: RelationshipType;
|
|
10
|
+
polymorphic: true;
|
|
11
|
+
targets: string[];
|
|
12
|
+
};
|
|
13
|
+
export type PrimaryKeyField = {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The agent read-model derived from a schema. Names are collection-scoped: relations and actions
|
|
19
|
+
* are keyed by `(collection, name)` so the same relation/action name on two collections stays
|
|
20
|
+
* distinct. The action allow-list is exactly the endpoint-map keys — endpoint-less actions are
|
|
21
|
+
* not exposed.
|
|
22
|
+
*/
|
|
23
|
+
export default class ReadModel {
|
|
24
|
+
private readonly collections;
|
|
25
|
+
private readonly relations;
|
|
26
|
+
private readonly actionEndpoints;
|
|
27
|
+
private readonly primaryKeys;
|
|
28
|
+
constructor(collections: ForestSchemaCollection[]);
|
|
29
|
+
private buildRelations;
|
|
30
|
+
private buildActionEndpoints;
|
|
31
|
+
private buildPrimaryKeys;
|
|
32
|
+
isCollectionAllowed(collection: string): boolean;
|
|
33
|
+
isRelationAllowed(collection: string, relation: string): boolean;
|
|
34
|
+
getRelationTarget(collection: string, relation: string): RelationTarget | undefined;
|
|
35
|
+
isActionAllowed(collection: string, action: string): boolean;
|
|
36
|
+
getActionEndpoints(): ActionEndpointsByCollection;
|
|
37
|
+
getPrimaryKeys(collection: string): PrimaryKeyField[];
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=read-model.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
function toRelationTarget(field) {
|
|
4
|
+
if (!field.relationship)
|
|
5
|
+
return null;
|
|
6
|
+
const type = field.relationship;
|
|
7
|
+
if (field.polymorphicReferencedModels && field.polymorphicReferencedModels.length > 0) {
|
|
8
|
+
return { type, polymorphic: true, targets: [...field.polymorphicReferencedModels] };
|
|
9
|
+
}
|
|
10
|
+
if (field.reference) {
|
|
11
|
+
// reference is `${foreignCollection}.${key}`; the collection name may itself contain dots
|
|
12
|
+
// (e.g. mongoose nested collections like `User.address`), so drop only the trailing key.
|
|
13
|
+
return { type, polymorphic: false, target: field.reference.split('.').slice(0, -1).join('.') };
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function deepFreeze(value) {
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
Object.values(value).forEach(deepFreeze);
|
|
20
|
+
Object.freeze(value);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The agent read-model derived from a schema. Names are collection-scoped: relations and actions
|
|
26
|
+
* are keyed by `(collection, name)` so the same relation/action name on two collections stays
|
|
27
|
+
* distinct. The action allow-list is exactly the endpoint-map keys — endpoint-less actions are
|
|
28
|
+
* not exposed.
|
|
29
|
+
*/
|
|
30
|
+
class ReadModel {
|
|
31
|
+
constructor(collections) {
|
|
32
|
+
this.collections = new Set();
|
|
33
|
+
this.relations = new Map();
|
|
34
|
+
// Null-prototype so an action/collection named like an Object.prototype member
|
|
35
|
+
// (`toString`, `constructor`, `__proto__`, …) can't falsely pass the allow-list.
|
|
36
|
+
this.actionEndpoints = Object.create(null);
|
|
37
|
+
this.primaryKeys = new Map();
|
|
38
|
+
for (const collection of collections) {
|
|
39
|
+
this.collections.add(collection.name);
|
|
40
|
+
this.buildRelations(collection);
|
|
41
|
+
this.buildActionEndpoints(collection);
|
|
42
|
+
this.buildPrimaryKeys(collection);
|
|
43
|
+
}
|
|
44
|
+
// Freeze the exposed structures so a consumer cannot mutate the shared cached read-model.
|
|
45
|
+
deepFreeze(this.actionEndpoints);
|
|
46
|
+
this.relations.forEach(byRelation => byRelation.forEach(deepFreeze));
|
|
47
|
+
this.primaryKeys.forEach(deepFreeze);
|
|
48
|
+
}
|
|
49
|
+
buildRelations(collection) {
|
|
50
|
+
const relations = new Map();
|
|
51
|
+
for (const field of collection.fields ?? []) {
|
|
52
|
+
const target = toRelationTarget(field);
|
|
53
|
+
if (target)
|
|
54
|
+
relations.set(field.field, target);
|
|
55
|
+
}
|
|
56
|
+
this.relations.set(collection.name, relations);
|
|
57
|
+
}
|
|
58
|
+
buildActionEndpoints(collection) {
|
|
59
|
+
const actions = collection.actions ?? [];
|
|
60
|
+
const withEndpoint = actions.filter(action => Boolean(action.endpoint));
|
|
61
|
+
if (withEndpoint.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
this.actionEndpoints[collection.name] = Object.create(null);
|
|
64
|
+
for (const action of withEndpoint) {
|
|
65
|
+
// Copy fields/hooks so a mutating consumer can't corrupt the shared cached schema, and
|
|
66
|
+
// default them defensively: the schema is cast from `Record<string, unknown>`, so a
|
|
67
|
+
// malformed action could omit them at runtime despite the declared type.
|
|
68
|
+
this.actionEndpoints[collection.name][action.name] = {
|
|
69
|
+
id: action.id,
|
|
70
|
+
name: action.name,
|
|
71
|
+
endpoint: action.endpoint,
|
|
72
|
+
hooks: { load: action.hooks?.load ?? false, change: [...(action.hooks?.change ?? [])] },
|
|
73
|
+
fields: (action.fields ?? []).map(field => ({ ...field })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
buildPrimaryKeys(collection) {
|
|
78
|
+
const keys = [];
|
|
79
|
+
for (const field of collection.fields ?? []) {
|
|
80
|
+
if (field.isPrimaryKey)
|
|
81
|
+
keys.push({ name: field.field, type: field.type });
|
|
82
|
+
}
|
|
83
|
+
this.primaryKeys.set(collection.name, keys);
|
|
84
|
+
}
|
|
85
|
+
isCollectionAllowed(collection) {
|
|
86
|
+
return this.collections.has(collection);
|
|
87
|
+
}
|
|
88
|
+
isRelationAllowed(collection, relation) {
|
|
89
|
+
return this.relations.get(collection)?.has(relation) ?? false;
|
|
90
|
+
}
|
|
91
|
+
getRelationTarget(collection, relation) {
|
|
92
|
+
return this.relations.get(collection)?.get(relation);
|
|
93
|
+
}
|
|
94
|
+
isActionAllowed(collection, action) {
|
|
95
|
+
return this.actionEndpoints[collection]?.[action] !== undefined;
|
|
96
|
+
}
|
|
97
|
+
getActionEndpoints() {
|
|
98
|
+
return this.actionEndpoints;
|
|
99
|
+
}
|
|
100
|
+
getPrimaryKeys(collection) {
|
|
101
|
+
return this.primaryKeys.get(collection) ?? [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.default = ReadModel;
|
|
105
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVhZC1tb2RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9yZWFkLW1vZGVsL3JlYWQtbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFXQSxTQUFTLGdCQUFnQixDQUFDLEtBQXdCO0lBQ2hELElBQUksQ0FBQyxLQUFLLENBQUMsWUFBWTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBRXJDLE1BQU0sSUFBSSxHQUFHLEtBQUssQ0FBQyxZQUFZLENBQUM7SUFFaEMsSUFBSSxLQUFLLENBQUMsMkJBQTJCLElBQUksS0FBSyxDQUFDLDJCQUEyQixDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUN0RixPQUFPLEVBQUUsSUFBSSxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLENBQUMsR0FBRyxLQUFLLENBQUMsMkJBQTJCLENBQUMsRUFBRSxDQUFDO0lBQ3RGLENBQUM7SUFFRCxJQUFJLEtBQUssQ0FBQyxTQUFTLEVBQUUsQ0FBQztRQUNwQiwwRkFBMEY7UUFDMUYseUZBQXlGO1FBQ3pGLE9BQU8sRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO0lBQ2pHLENBQUM7SUFFRCxPQUFPLElBQUksQ0FBQztBQUNkLENBQUM7QUFFRCxTQUFTLFVBQVUsQ0FBSSxLQUFRO0lBQzdCLElBQUksS0FBSyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsRUFBRSxDQUFDO1FBQ3ZDLE1BQU0sQ0FBQyxNQUFNLENBQUMsS0FBZ0MsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUNwRSxNQUFNLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3ZCLENB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
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SchemaFetcher } from './forest-schema-client';
|
|
2
|
+
import type { Metrics } from '../ports/metrics-port';
|
|
3
|
+
import type { ForestSchemaCollection } from '@forestadmin/forestadmin-client';
|
|
4
|
+
export declare const ONE_DAY_MS: number;
|
|
5
|
+
export declare const SCHEMA_CACHE_REFRESH_ERROR = "schema_cache_refresh_error";
|
|
6
|
+
export declare const SCHEMA_CACHE_AGE_SECONDS = "schema_cache_age_seconds";
|
|
7
|
+
export interface SchemaCacheOptions {
|
|
8
|
+
fetcher: SchemaFetcher;
|
|
9
|
+
metrics: Metrics;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Caches the agent schema for 24h. The TTL only *triggers* a refresh attempt: the last good
|
|
15
|
+
* schema keeps being served until a refresh succeeds. A cold cache (no last good) surfaces a
|
|
16
|
+
* typed `SchemaUnavailableError`. A monotonic `revision` increments on each successful refresh so
|
|
17
|
+
* a consumer can detect a new schema generation explicitly.
|
|
18
|
+
*/
|
|
19
|
+
export default class SchemaCache {
|
|
20
|
+
private readonly fetcher;
|
|
21
|
+
private readonly metrics;
|
|
22
|
+
private readonly now;
|
|
23
|
+
private readonly ttlMs;
|
|
24
|
+
private entry;
|
|
25
|
+
private inFlight;
|
|
26
|
+
private revisionValue;
|
|
27
|
+
constructor({ fetcher, metrics, now, ttlMs }: SchemaCacheOptions);
|
|
28
|
+
get(): Promise<ForestSchemaCollection[]>;
|
|
29
|
+
ageSeconds(): number | undefined;
|
|
30
|
+
get revision(): number;
|
|
31
|
+
private refresh;
|
|
32
|
+
private doRefresh;
|
|
33
|
+
private emitAge;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=schema-cache.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
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.SCHEMA_CACHE_AGE_SECONDS = exports.SCHEMA_CACHE_REFRESH_ERROR = exports.ONE_DAY_MS = void 0;
|
|
7
|
+
const errors_1 = __importDefault(require("./errors"));
|
|
8
|
+
exports.ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
exports.SCHEMA_CACHE_REFRESH_ERROR = 'schema_cache_refresh_error';
|
|
10
|
+
exports.SCHEMA_CACHE_AGE_SECONDS = 'schema_cache_age_seconds';
|
|
11
|
+
/**
|
|
12
|
+
* Caches the agent schema for 24h. The TTL only *triggers* a refresh attempt: the last good
|
|
13
|
+
* schema keeps being served until a refresh succeeds. A cold cache (no last good) surfaces a
|
|
14
|
+
* typed `SchemaUnavailableError`. A monotonic `revision` increments on each successful refresh so
|
|
15
|
+
* a consumer can detect a new schema generation explicitly.
|
|
16
|
+
*/
|
|
17
|
+
class SchemaCache {
|
|
18
|
+
constructor({ fetcher, metrics, now = Date.now, ttlMs = exports.ONE_DAY_MS }) {
|
|
19
|
+
this.entry = null;
|
|
20
|
+
this.inFlight = null;
|
|
21
|
+
this.revisionValue = 0;
|
|
22
|
+
this.fetcher = fetcher;
|
|
23
|
+
this.metrics = metrics;
|
|
24
|
+
this.now = now;
|
|
25
|
+
this.ttlMs = ttlMs;
|
|
26
|
+
}
|
|
27
|
+
async get() {
|
|
28
|
+
if (this.entry && this.now() - this.entry.fetchedAt < this.ttlMs) {
|
|
29
|
+
this.emitAge();
|
|
30
|
+
return this.entry.collections;
|
|
31
|
+
}
|
|
32
|
+
return this.refresh();
|
|
33
|
+
}
|
|
34
|
+
ageSeconds() {
|
|
35
|
+
if (!this.entry)
|
|
36
|
+
return undefined;
|
|
37
|
+
return Math.floor((this.now() - this.entry.fetchedAt) / 1000);
|
|
38
|
+
}
|
|
39
|
+
get revision() {
|
|
40
|
+
return this.revisionValue;
|
|
41
|
+
}
|
|
42
|
+
async refresh() {
|
|
43
|
+
if (!this.inFlight) {
|
|
44
|
+
this.inFlight = this.doRefresh().finally(() => {
|
|
45
|
+
this.inFlight = null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return this.inFlight;
|
|
49
|
+
}
|
|
50
|
+
async doRefresh() {
|
|
51
|
+
try {
|
|
52
|
+
const collections = await this.fetcher.fetchSchema();
|
|
53
|
+
// An agent always exposes collections, so an empty result is far likelier a broken response
|
|
54
|
+
// than a valid state. Caching it would silently deny everything for 24h — treat it as a
|
|
55
|
+
// failed fetch and fall through to the failure path below.
|
|
56
|
+
if (collections.length === 0)
|
|
57
|
+
throw new Error('Forest returned an empty schema');
|
|
58
|
+
this.entry = { collections, fetchedAt: this.now() };
|
|
59
|
+
this.revisionValue += 1;
|
|
60
|
+
this.emitAge();
|
|
61
|
+
return collections;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.metrics.increment(exports.SCHEMA_CACHE_REFRESH_ERROR);
|
|
65
|
+
// Warm cache: keep serving the last good schema (stale), do not poison — the next read
|
|
66
|
+
// re-attempts because `fetchedAt` is unchanged and the entry stays expired.
|
|
67
|
+
if (this.entry) {
|
|
68
|
+
this.emitAge();
|
|
69
|
+
return this.entry.collections;
|
|
70
|
+
}
|
|
71
|
+
// Cold cache: nothing to serve.
|
|
72
|
+
throw new errors_1.default(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
emitAge() {
|
|
76
|
+
const age = this.ageSeconds();
|
|
77
|
+
if (age !== undefined)
|
|
78
|
+
this.metrics.gauge(exports.SCHEMA_CACHE_AGE_SECONDS, age);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.default = SchemaCache;
|
|
82
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZW1hLWNhY2hlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlYWQtbW9kZWwvc2NoZW1hLWNhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQUlBLHNEQUE4QztBQUVqQyxRQUFBLFVBQVUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUM7QUFFakMsUUFBQSwwQkFBMEIsR0FBRyw0QkFBNEIsQ0FBQztBQUMxRCxRQUFBLHdCQUF3QixHQUFHLDBCQUEwQixDQUFDO0FBY25FOzs7OztHQUtHO0FBQ0gsTUFBcUIsV0FBVztJQVU5QixZQUFZLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxLQUFLLEdBQUcsa0JBQVUsRUFBc0I7UUFKaEYsVUFBSyxHQUFzQixJQUFJLENBQUM7UUFDaEMsYUFBUSxHQUE2QyxJQUFJLENBQUM7UUFDMUQsa0JBQWEsR0FBRyxDQUFDLENBQUM7UUFHeEIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFDdkIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFDdkIsSUFBSSxDQUFDLEdBQUcsR0FBRyxHQUFHLENBQUM7UUFDZixJQUFJLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQztJQUNyQixDQUFDO0lBRUQsS0FBSyxDQUFDLEdBQUc7UUFDUCxJQUFJLElBQUksQ0FBQyxLQUFLLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNqRSxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFFZixPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDO1FBQ2hDLENBQUM7UUFFRCxPQUFPLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUN4QixDQUFDO0lBRUQsVUFBVTtRQUNSLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSztZQUFFLE9BQU8sU0FBUyxDQUFDO1FBRWxDLE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQ2hFLENBQUM7SUFFRCxJQUFJLFFBQVE7UUFDVixPQUFPLElBQUksQ0FBQyxhQUFhLENBQUM7SUFDNUIsQ0FBQztJQUVPLEtBQUssQ0FBQyxPQUFPO1FBQ25CLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDbkIsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRTtnQkFDNUMsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUM7WUFDdkIsQ0FBQyxDQUFDLENBQUM7UUFDTCxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDO0lBQ3ZCLENBQUM7SUFFTyxLQUFLLENBQUMsU0FBUztRQUNyQixJQUFJLENBQUM7WUFDSCxNQUFNLFdBQVcsR0FBRyxNQUFNLElBQUksQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUM7WUFFckQsNEZBQTRGO1lBQzVGLHdGQUF3RjtZQUN4RiwyREFBMkQ7WUFDM0QsSUFBSSxXQUFXLENBQUMsTUFBTSxLQUFLLENBQUM7Z0JBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQ0FBaUMsQ0FBQyxDQUFDO1lBRWpGLElBQUksQ0FBQyxLQUFLLEdBQUcsRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDO1lBQ3BELElBQUksQ0FBQyxhQUFhLElBQUksQ0FBQyxDQUFDO1lBQ3hCLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUVmLE9BQU8sV0FBVyxDQUFDO1FBQ3JCLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsa0NBQTBCLENBQUMsQ0FBQztZQUVuRCx1RkFBdUY7WUFDdkYsNEVBQTRFO1lBQzVFLElBQUksSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO2dCQUNmLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztnQkFFZixPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDO1lBQ2hDLENBQUM7WUFFRCxnQ0FBZ0M7WUFDaEMsTUFBTSxJQUFJLGdCQUFzQixDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzFDLENBQUM7SUFDSCxDQUFDO0lBRU8sT0FBTztRQUNiLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUM5QixJQUFJLEdBQUcsS0FBSyxTQUFTO1lBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsZ0NBQXdCLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDM0UsQ0FBQztDQUNGO0FBakZELDhCQWlGQyJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestadmin/agent-bff",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forest-bff": "dist/cli.js"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"test": "jest"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@forestadmin/agent-client": "1.10.1",
|
|
34
35
|
"@forestadmin/forestadmin-client": "1.40.4",
|
|
35
36
|
"@koa/bodyparser": "^6.1.0",
|
|
36
37
|
"jsonwebtoken": "^9.0.3",
|