@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.
Files changed (40) hide show
  1. package/dist/adapters/console-metrics.d.ts +4 -0
  2. package/dist/adapters/console-metrics.js +18 -0
  3. package/dist/adapters/safe-metrics.d.ts +8 -0
  4. package/dist/adapters/safe-metrics.js +29 -0
  5. package/dist/cli-core.js +17 -2
  6. package/dist/data/agent-data-client.d.ts +17 -0
  7. package/dist/data/agent-data-client.js +32 -0
  8. package/dist/data/agent-query.d.ts +36 -0
  9. package/dist/data/agent-query.js +144 -0
  10. package/dist/data/data-routes-middleware.d.ts +12 -0
  11. package/dist/data/data-routes-middleware.js +159 -0
  12. package/dist/data/pack-id.d.ts +9 -0
  13. package/dist/data/pack-id.js +37 -0
  14. package/dist/data/response-mappers.d.ts +20 -0
  15. package/dist/data/response-mappers.js +38 -0
  16. package/dist/http/agent-error-mapper.d.ts +18 -0
  17. package/dist/http/agent-error-mapper.js +114 -0
  18. package/dist/http/bff-local-errors.d.ts +12 -0
  19. package/dist/http/bff-local-errors.js +44 -0
  20. package/dist/ports/metrics-port.d.ts +6 -0
  21. package/dist/ports/metrics-port.js +3 -0
  22. package/dist/read-model/action-endpoint-resolver.d.ts +23 -0
  23. package/dist/read-model/action-endpoint-resolver.js +36 -0
  24. package/dist/read-model/agent-capabilities-fetcher.d.ts +11 -0
  25. package/dist/read-model/agent-capabilities-fetcher.js +13 -0
  26. package/dist/read-model/capabilities-cache.d.ts +27 -0
  27. package/dist/read-model/capabilities-cache.js +47 -0
  28. package/dist/read-model/create-read-model.d.ts +17 -0
  29. package/dist/read-model/create-read-model.js +24 -0
  30. package/dist/read-model/errors.d.ts +5 -0
  31. package/dist/read-model/errors.js +11 -0
  32. package/dist/read-model/forest-schema-client.d.ts +14 -0
  33. package/dist/read-model/forest-schema-client.js +13 -0
  34. package/dist/read-model/read-model-store.d.ts +20 -0
  35. package/dist/read-model/read-model-store.js +43 -0
  36. package/dist/read-model/read-model.d.ts +39 -0
  37. package/dist/read-model/read-model.js +105 -0
  38. package/dist/read-model/schema-cache.d.ts +35 -0
  39. package/dist/read-model/schema-cache.js +82 -0
  40. 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.5.0",
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",