@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.
Files changed (57) hide show
  1. package/README.md +15 -1
  2. package/dist/cli-core.d.ts +3 -0
  3. package/dist/cli-core.js +69 -10
  4. package/dist/cli-dispatch.d.ts +8 -2
  5. package/dist/cli-dispatch.js +90 -9
  6. package/dist/data/agent-query.d.ts +2 -0
  7. package/dist/data/agent-query.js +28 -17
  8. package/dist/data/data-routes-middleware.js +66 -26
  9. package/dist/data/pack-id.d.ts +1 -0
  10. package/dist/data/pack-id.js +4 -3
  11. package/dist/http/agent-error-mapper.js +15 -7
  12. package/dist/http/bff-http-error.d.ts +6 -1
  13. package/dist/http/bff-http-error.js +5 -4
  14. package/dist/http/bff-local-errors.d.ts +2 -0
  15. package/dist/http/bff-local-errors.js +12 -4
  16. package/dist/openapi/collect-unfolding.d.ts +23 -0
  17. package/dist/openapi/collect-unfolding.js +144 -0
  18. package/dist/openapi/component-pool.d.ts +18 -0
  19. package/dist/openapi/component-pool.js +28 -0
  20. package/dist/openapi/field-schemas.d.ts +9 -0
  21. package/dist/openapi/field-schemas.js +43 -0
  22. package/dist/openapi/names.d.ts +16 -0
  23. package/dist/openapi/names.js +28 -0
  24. package/dist/openapi/openapi-document.d.ts +2 -1
  25. package/dist/openapi/openapi-document.js +112 -66
  26. package/dist/openapi/openapi-routes.d.ts +4 -1
  27. package/dist/openapi/openapi-routes.js +46 -9
  28. package/dist/openapi/schemas.d.ts +14 -1
  29. package/dist/openapi/schemas.js +11 -10
  30. package/dist/openapi/unfolded-document.d.ts +28 -0
  31. package/dist/openapi/unfolded-document.js +52 -0
  32. package/dist/openapi/unfolded-paths.d.ts +20 -0
  33. package/dist/openapi/unfolded-paths.js +408 -0
  34. package/dist/openapi/unfolding.d.ts +66 -0
  35. package/dist/openapi/unfolding.js +17 -0
  36. package/dist/permissions/build-permission-hints.d.ts +40 -0
  37. package/dist/permissions/build-permission-hints.js +73 -0
  38. package/dist/permissions/can-role-perform.d.ts +12 -0
  39. package/dist/permissions/can-role-perform.js +18 -0
  40. package/dist/permissions/permissions-cache.d.ts +24 -0
  41. package/dist/permissions/permissions-cache.js +51 -0
  42. package/dist/permissions/permissions-client.d.ts +21 -0
  43. package/dist/permissions/permissions-client.js +27 -0
  44. package/dist/permissions/permissions-routes-middleware.d.ts +13 -0
  45. package/dist/permissions/permissions-routes-middleware.js +99 -0
  46. package/dist/read-model/agent-capabilities-fetcher.d.ts +8 -3
  47. package/dist/read-model/agent-capabilities-fetcher.js +15 -7
  48. package/dist/read-model/capabilities-cache.d.ts +11 -1
  49. package/dist/read-model/capabilities-cache.js +1 -1
  50. package/dist/read-model/read-model.d.ts +15 -0
  51. package/dist/read-model/read-model.js +39 -2
  52. package/dist/validation/capabilities-validator.d.ts +9 -0
  53. package/dist/validation/capabilities-validator.js +3 -1
  54. package/dist/validation/operator-normalizer.d.ts +6 -0
  55. package/dist/validation/operator-normalizer.js +11 -1
  56. package/dist/validation/validation-errors.js +7 -5
  57. package/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ import type { FieldType } from '../read-model/capabilities-cache';
2
+ import type { PrimaryKeyField } from '../read-model/read-model';
3
+ import type { Operator } from '@forestadmin/datasource-toolkit';
4
+ /**
5
+ * The snapshot the OpenAPI generator unfolds. It is plain data on purpose: collecting it hits the
6
+ * network (schema + capabilities per collection), generating from it does not, so the route and the
7
+ * CLI can be proven to emit the same document by feeding them one snapshot.
8
+ */
9
+ export interface Unfolding {
10
+ collections: UnfoldedCollection[];
11
+ }
12
+ export interface UnfoldedCollection {
13
+ name: string;
14
+ fields: CollectionFields;
15
+ primaryKeys: PrimaryKeyField[];
16
+ /** To-many relations whose foreign collection is exposed too — the only ones with a route. */
17
+ relations: UnfoldedRelation[];
18
+ actions: UnfoldedAction[];
19
+ }
20
+ export interface UnfoldedRelation {
21
+ name: string;
22
+ foreignCollection: string;
23
+ }
24
+ /**
25
+ * `projectable` is every capability field; `filterable` is the subset carrying operators. They
26
+ * differ: a `ManyToOne` is reported by the agent without operators, so projecting or sorting on it
27
+ * works while filtering on it answers 422 field_not_filterable.
28
+ *
29
+ * `degraded` is set when the field set could not be established. The collection keeps its paths, but
30
+ * their field schemas stay free-form strings — an empty enum would forbid every valid call.
31
+ */
32
+ export interface CollectionFields {
33
+ projectable: string[];
34
+ filterable: FilterableField[];
35
+ degraded: DegradedReason | null;
36
+ }
37
+ /**
38
+ * A field the agent reports at least one operator for, with that operator set — the very set the
39
+ * runtime validates a filter leaf against. Normalized to canonical PascalCase and to the
40
+ * `allOperators` order at collect time, so the same capabilities answer always yields the same
41
+ * document whatever order the agent serialized the operators in.
42
+ */
43
+ export interface FilterableField {
44
+ name: string;
45
+ operators: Operator[];
46
+ }
47
+ export type DegradedReason = 'capabilities_unavailable' | 'no_fields';
48
+ /** True when at least one collection went out without its field set. */
49
+ export declare function hasDegradedCollection(unfolding: Unfolding): boolean;
50
+ /**
51
+ * True when NO collection got its field set — the signature of an unreachable agent rather than of
52
+ * one odd collection. An empty schema is not degraded: there was nothing to enumerate.
53
+ */
54
+ export declare function isFullyDegraded(unfolding: Unfolding): boolean;
55
+ export interface UnfoldedAction {
56
+ name: string;
57
+ fields: UnfoldedActionField[];
58
+ }
59
+ export interface UnfoldedActionField {
60
+ name: string;
61
+ type: FieldType;
62
+ isRequired: boolean;
63
+ enums: string[] | null;
64
+ }
65
+ export type { FieldType };
66
+ //# sourceMappingURL=unfolding.d.ts.map
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasDegradedCollection = hasDegradedCollection;
4
+ exports.isFullyDegraded = isFullyDegraded;
5
+ /** True when at least one collection went out without its field set. */
6
+ function hasDegradedCollection(unfolding) {
7
+ return unfolding.collections.some(collection => collection.fields.degraded !== null);
8
+ }
9
+ /**
10
+ * True when NO collection got its field set — the signature of an unreachable agent rather than of
11
+ * one odd collection. An empty schema is not degraded: there was nothing to enumerate.
12
+ */
13
+ function isFullyDegraded(unfolding) {
14
+ return (unfolding.collections.length > 0 &&
15
+ unfolding.collections.every(collection => collection.fields.degraded !== null));
16
+ }
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidW5mb2xkaW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL29wZW5hcGkvdW5mb2xkaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBdURBLHNEQUVDO0FBTUQsMENBS0M7QUFkRCx3RUFBd0U7QUFDeEUsU0FBZ0IscUJBQXFCLENBQUMsU0FBb0I7SUFDeEQsT0FBTyxTQUFTLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsUUFBUSxLQUFLLElBQUksQ0FBQyxDQUFDO0FBQ3ZGLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxTQUFnQixlQUFlLENBQUMsU0FBb0I7SUFDbEQsT0FBTyxDQUNMLFNBQVMsQ0FBQyxXQUFXLENBQUMsTUFBTSxHQUFHLENBQUM7UUFDaEMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLFFBQVEsS0FBSyxJQUFJLENBQUMsQ0FDL0UsQ0FBQztBQUNKLENBQUMifQ==
@@ -0,0 +1,40 @@
1
+ import type ReadModel from '../read-model/read-model';
2
+ import type { ActionPermissions } from '@forestadmin/forestadmin-client';
3
+ export declare const DISPLAY_HINT_FINALITY = "display_hint";
4
+ export interface CrudHints {
5
+ browse: boolean;
6
+ read: boolean;
7
+ edit: boolean;
8
+ add: boolean;
9
+ delete: boolean;
10
+ export: boolean;
11
+ }
12
+ export interface ActionHints {
13
+ collection: string;
14
+ name: string;
15
+ visible: boolean;
16
+ requiresApprovalHint: boolean;
17
+ canApproveHint: boolean;
18
+ canSelfApproveHint: boolean;
19
+ finality: typeof DISPLAY_HINT_FINALITY;
20
+ }
21
+ export interface VisibleActionRef {
22
+ collection: string;
23
+ name: string;
24
+ }
25
+ export interface CollectionHints {
26
+ crud: CrudHints;
27
+ actions: Record<string, ActionHints>;
28
+ }
29
+ export interface PermissionHints {
30
+ collections: Record<string, CollectionHints>;
31
+ visibleActions: VisibleActionRef[];
32
+ }
33
+ export interface BuildPermissionHintsParams {
34
+ actionPermissions: ActionPermissions;
35
+ roleId: number;
36
+ readModel: ReadModel;
37
+ collections: string[];
38
+ }
39
+ export default function buildPermissionHints(params: BuildPermissionHintsParams): PermissionHints;
40
+ //# sourceMappingURL=build-permission-hints.d.ts.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DISPLAY_HINT_FINALITY = void 0;
4
+ exports.default = buildPermissionHints;
5
+ const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
6
+ const can_role_perform_1 = require("./can-role-perform");
7
+ exports.DISPLAY_HINT_FINALITY = 'display_hint';
8
+ function buildCrudHints(permissions, roleId, collection) {
9
+ const can = (action) => (0, can_role_perform_1.canRolePerformCollectionAction)(permissions, roleId, action, collection);
10
+ return {
11
+ browse: can(forestadmin_client_1.CollectionActionEvent.Browse),
12
+ read: can(forestadmin_client_1.CollectionActionEvent.Read),
13
+ edit: can(forestadmin_client_1.CollectionActionEvent.Edit),
14
+ add: can(forestadmin_client_1.CollectionActionEvent.Add),
15
+ delete: can(forestadmin_client_1.CollectionActionEvent.Delete),
16
+ export: can(forestadmin_client_1.CollectionActionEvent.Export),
17
+ };
18
+ }
19
+ function visibleWithoutApprovalHints(collection, name) {
20
+ return {
21
+ collection,
22
+ name,
23
+ visible: true,
24
+ requiresApprovalHint: false,
25
+ canApproveHint: false,
26
+ canSelfApproveHint: false,
27
+ finality: exports.DISPLAY_HINT_FINALITY,
28
+ };
29
+ }
30
+ function normalModeAction(permissions, roleId, collection, name) {
31
+ const can = (event) => (0, can_role_perform_1.canRolePerformCustomAction)({
32
+ permissions,
33
+ roleId,
34
+ event,
35
+ actionName: name,
36
+ collectionName: collection,
37
+ });
38
+ return {
39
+ collection,
40
+ name,
41
+ visible: can(forestadmin_client_1.CustomActionEvent.Trigger),
42
+ requiresApprovalHint: can(forestadmin_client_1.CustomActionEvent.RequireApproval),
43
+ canApproveHint: can(forestadmin_client_1.CustomActionEvent.Approve),
44
+ canSelfApproveHint: can(forestadmin_client_1.CustomActionEvent.SelfApprove),
45
+ finality: exports.DISPLAY_HINT_FINALITY,
46
+ };
47
+ }
48
+ function buildCollectionHints(params, collection) {
49
+ const { permissions, roleId, readModel } = params;
50
+ const { isDevelopment } = permissions;
51
+ const schemaExposedActionNames = Object.keys(readModel.getActionEndpoints()[collection] ?? {});
52
+ const actions = Object.create(null);
53
+ for (const name of schemaExposedActionNames) {
54
+ actions[name] = isDevelopment
55
+ ? visibleWithoutApprovalHints(collection, name)
56
+ : normalModeAction(permissions, roleId, collection, name);
57
+ }
58
+ return { crud: buildCrudHints(permissions, roleId, collection), actions };
59
+ }
60
+ function buildPermissionHints(params) {
61
+ const permissions = params.actionPermissions;
62
+ const collections = Object.create(null);
63
+ const visibleActions = [];
64
+ for (const collection of params.collections) {
65
+ const hints = buildCollectionHints({ ...params, permissions }, collection);
66
+ collections[collection] = hints;
67
+ visibleActions.push(...Object.values(hints.actions)
68
+ .filter(action => action.visible)
69
+ .map(({ name }) => ({ collection, name })));
70
+ }
71
+ return { collections, visibleActions };
72
+ }
73
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGQtcGVybWlzc2lvbi1oaW50cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wZXJtaXNzaW9ucy9idWlsZC1wZXJtaXNzaW9uLWhpbnRzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQTZIQSx1Q0FnQkM7QUExSUQsd0VBQTJGO0FBRTNGLHlEQUFnRztBQUVuRixRQUFBLHFCQUFxQixHQUFHLGNBQWMsQ0FBQztBQTJDcEQsU0FBUyxjQUFjLENBQ3JCLFdBQThCLEVBQzlCLE1BQWMsRUFDZCxVQUFrQjtJQUVsQixNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQTZCLEVBQUUsRUFBRSxDQUM1QyxJQUFBLGlEQUE4QixFQUFDLFdBQVcsRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBRTFFLE9BQU87UUFDTCxNQUFNLEVBQUUsR0FBRyxDQUFDLDBDQUFxQixDQUFDLE1BQU0sQ0FBQztRQUN6QyxJQUFJLEVBQUUsR0FBRyxDQUFDLDBDQUFxQixDQUFDLElBQUksQ0FBQztRQUNyQyxJQUFJLEVBQUUsR0FBRyxDQUFDLDBDQUFxQixDQUFDLElBQUksQ0FBQztRQUNyQyxHQUFHLEVBQUUsR0FBRyxDQUFDLDBDQUFxQixDQUFDLEdBQUcsQ0FBQztRQUNuQyxNQUFNLEVBQUUsR0FBRyxDQUFDLDBDQUFxQixDQUFDLE1BQU0sQ0FBQztRQUN6QyxNQUFNLEVBQUUsR0FBRyxDQUFDLDBDQUFxQixDQUFDLE1BQU0sQ0FBQztLQUMxQyxDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsMkJBQTJCLENBQUMsVUFBa0IsRUFBRSxJQUFZO0lBQ25FLE9BQU87UUFDTCxVQUFVO1FBQ1YsSUFBSTtRQUNKLE9BQU8sRUFBRSxJQUFJO1FBQ2Isb0JBQW9CLEVBQUUsS0FBSztRQUMzQixjQUFjLEVBQUUsS0FBSztRQUNyQixrQkFBa0IsRUFBRSxLQUFLO1FBQ3pCLFFBQVEsRUFBRSw2QkFBcUI7S0FDaEMsQ0FBQztBQUNKLENB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
@@ -0,0 +1,12 @@
1
+ import type { ActionPermissions, CollectionActionEvent, CustomActionEvent } from '@forestadmin/forestadmin-client';
2
+ export declare function isActionIdentifierAllowedForRole(permissions: ActionPermissions, roleId: number, actionName: string): boolean;
3
+ export declare function canRolePerformCollectionAction(permissions: ActionPermissions, roleId: number, action: CollectionActionEvent, collectionName: string): boolean;
4
+ export interface CanRolePerformCustomActionParams {
5
+ permissions: ActionPermissions;
6
+ roleId: number;
7
+ event: CustomActionEvent;
8
+ actionName: string;
9
+ collectionName: string;
10
+ }
11
+ export declare function canRolePerformCustomAction({ permissions, roleId, event, actionName, collectionName, }: CanRolePerformCustomActionParams): boolean;
12
+ //# sourceMappingURL=can-role-perform.d.ts.map
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isActionIdentifierAllowedForRole = isActionIdentifierAllowedForRole;
4
+ exports.canRolePerformCollectionAction = canRolePerformCollectionAction;
5
+ exports.canRolePerformCustomAction = canRolePerformCustomAction;
6
+ const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
7
+ function isActionIdentifierAllowedForRole(permissions, roleId, actionName) {
8
+ return Boolean(permissions.isDevelopment ||
9
+ permissions.actionsGloballyAllowed.has(actionName) ||
10
+ permissions.actionsByRole.get(actionName)?.allowedRoles.has(roleId));
11
+ }
12
+ function canRolePerformCollectionAction(permissions, roleId, action, collectionName) {
13
+ return isActionIdentifierAllowedForRole(permissions, roleId, (0, forestadmin_client_1.generateCollectionActionIdentifier)(action, collectionName));
14
+ }
15
+ function canRolePerformCustomAction({ permissions, roleId, event, actionName, collectionName, }) {
16
+ return isActionIdentifierAllowedForRole(permissions, roleId, (0, forestadmin_client_1.generateCustomActionIdentifier)(event, actionName, collectionName));
17
+ }
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FuLXJvbGUtcGVyZm9ybS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wZXJtaXNzaW9ucy9jYW4tcm9sZS1wZXJmb3JtLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBV0EsNEVBVUM7QUFFRCx3RUFXQztBQVVELGdFQVlDO0FBbERELHdFQUd5QztBQUV6QyxTQUFnQixnQ0FBZ0MsQ0FDOUMsV0FBOEIsRUFDOUIsTUFBYyxFQUNkLFVBQWtCO0lBRWxCLE9BQU8sT0FBTyxDQUNaLFdBQVcsQ0FBQyxhQUFhO1FBQ3ZCLFdBQVcsQ0FBQyxzQkFBc0IsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDO1FBQ2xELFdBQVcsQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxFQUFFLFlBQVksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQ3RFLENBQUM7QUFDSixDQUFDO0FBRUQsU0FBZ0IsOEJBQThCLENBQzVDLFdBQThCLEVBQzlCLE1BQWMsRUFDZCxNQUE2QixFQUM3QixjQUFzQjtJQUV0QixPQUFPLGdDQUFnQyxDQUNyQyxXQUFXLEVBQ1gsTUFBTSxFQUNOLElBQUEsdURBQWtDLEVBQUMsTUFBTSxFQUFFLGNBQWMsQ0FBQyxDQUMzRCxDQUFDO0FBQ0osQ0FBQztBQVVELFNBQWdCLDBCQUEwQixDQUFDLEVBQ3pDLFdBQVcsRUFDWCxNQUFNLEVBQ04sS0FBSyxFQUNMLFVBQVUsRUFDVixjQUFjLEdBQ21CO0lBQ2pDLE9BQU8sZ0NBQWdDLENBQ3JDLFdBQVcsRUFDWCxNQUFNLEVBQ04sSUFBQSxtREFBOEIsRUFBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLGNBQWMsQ0FBQyxDQUNsRSxDQUFDO0FBQ0osQ0FBQyJ9
@@ -0,0 +1,24 @@
1
+ import type { ActionPermissions, UserPermissionV4 } from '@forestadmin/forestadmin-client';
2
+ export declare const PERMISSIONS_CACHE_TTL_MS: number;
3
+ export interface PermissionsCacheOptions {
4
+ now?: () => number;
5
+ ttlMs?: number;
6
+ }
7
+ export interface EvaluatedPermissions {
8
+ actionPermissions: ActionPermissions;
9
+ users: UserPermissionV4[];
10
+ }
11
+ export default class PermissionsCache {
12
+ private readonly now;
13
+ private readonly ttlMs;
14
+ private entry;
15
+ constructor({ now, ttlMs }?: PermissionsCacheOptions);
16
+ wasRejectedByLatestFetch(userId: number): boolean;
17
+ rememberRejected(userId: number): void;
18
+ getFresh(): EvaluatedPermissions | undefined;
19
+ set(permissions: EvaluatedPermissions): void;
20
+ get size(): number;
21
+ clear(): void;
22
+ private getFreshEntry;
23
+ }
24
+ //# sourceMappingURL=permissions-cache.d.ts.map
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PERMISSIONS_CACHE_TTL_MS = void 0;
4
+ exports.PERMISSIONS_CACHE_TTL_MS = 15 * 60 * 1000;
5
+ function sameUsers(previous, next) {
6
+ if (previous.length !== next.length)
7
+ return false;
8
+ const previousRoleById = new Map(previous.map(user => [user.id, user.roleId]));
9
+ return next.every(user => previousRoleById.get(user.id) === user.roleId);
10
+ }
11
+ class PermissionsCache {
12
+ constructor({ now = Date.now, ttlMs = exports.PERMISSIONS_CACHE_TTL_MS } = {}) {
13
+ this.now = now;
14
+ this.ttlMs = ttlMs;
15
+ }
16
+ wasRejectedByLatestFetch(userId) {
17
+ return this.getFreshEntry()?.rejectedUserIds.has(userId) ?? false;
18
+ }
19
+ rememberRejected(userId) {
20
+ this.getFreshEntry()?.rejectedUserIds.add(userId);
21
+ }
22
+ getFresh() {
23
+ return this.getFreshEntry()?.permissions;
24
+ }
25
+ set(permissions) {
26
+ const previous = this.getFreshEntry();
27
+ const carriesTheSameUsers = previous !== undefined && sameUsers(previous.permissions.users, permissions.users);
28
+ this.entry = {
29
+ permissions,
30
+ storedAt: this.now(),
31
+ rejectedUserIds: carriesTheSameUsers ? previous.rejectedUserIds : new Set(),
32
+ };
33
+ }
34
+ get size() {
35
+ return this.entry ? 1 : 0;
36
+ }
37
+ clear() {
38
+ this.entry = undefined;
39
+ }
40
+ getFreshEntry() {
41
+ if (!this.entry)
42
+ return undefined;
43
+ if (this.now() - this.entry.storedAt >= this.ttlMs) {
44
+ this.entry = undefined;
45
+ return undefined;
46
+ }
47
+ return this.entry;
48
+ }
49
+ }
50
+ exports.default = PermissionsCache;
51
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVybWlzc2lvbnMtY2FjaGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcGVybWlzc2lvbnMvcGVybWlzc2lvbnMtY2FjaGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBRWEsUUFBQSx3QkFBd0IsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQWtCdkQsU0FBUyxTQUFTLENBQUMsUUFBNEIsRUFBRSxJQUF3QjtJQUN2RSxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEtBQUssQ0FBQztJQUVsRCxNQUFNLGdCQUFnQixHQUFHLElBQUksR0FBRyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUUvRSxPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxLQUFLLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUMzRSxDQUFDO0FBRUQsTUFBcUIsZ0JBQWdCO0lBTW5DLFlBQVksRUFBRSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxLQUFLLEdBQUcsZ0NBQXdCLEtBQThCLEVBQUU7UUFDNUYsSUFBSSxDQUFDLEdBQUcsR0FBRyxHQUFHLENBQUM7UUFDZixJQUFJLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQztJQUNyQixDQUFDO0lBRUQsd0JBQXdCLENBQUMsTUFBYztRQUNyQyxPQUFPLElBQUksQ0FBQyxhQUFhLEVBQUUsRUFBRSxlQUFlLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEtBQUssQ0FBQztJQUNwRSxDQUFDO0lBRUQsZ0JBQWdCLENBQUMsTUFBYztRQUM3QixJQUFJLENBQUMsYUFBYSxFQUFFLEVBQUUsZUFBZSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUNwRCxDQUFDO0lBRUQsUUFBUTtRQUNOLE9BQU8sSUFBSSxDQUFDLGFBQWEsRUFBRSxFQUFFLFdBQVcsQ0FBQztJQUMzQyxDQUFDO0lBRUQsR0FBRyxDQUFDLFdBQWlDO1FBQ25DLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQztRQUN0QyxNQUFNLG1CQUFtQixHQUN2QixRQUFRLEtBQUssU0FBUyxJQUFJLFNBQVMsQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDLEtBQUssRUFBRSxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFckYsSUFBSSxDQUFDLEtBQUssR0FBRztZQUNYLFdBQVc7WUFDWCxRQUFRLEVBQUUsSUFBSSxDQUFDLEdBQUcsRUFBRTtZQUNwQixlQUFlLEVBQUUsbUJBQW1CLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDLElBQUksR0FBRyxFQUFVO1NBQ3BGLENBQUM7SUFDSixDQUFDO0lBRUQsSUFBSSxJQUFJO1FBQ04sT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM1QixDQUFDO0lBRUQsS0FBSztRQUNILElBQUksQ0FBQyxLQUFLLEdBQUcsU0FBUyxDQUFDO0lBQ3pCLENBQUM7SUFFTyxhQUFhO1FBQ25CLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSztZQUFFLE9BQU8sU0FBUyxDQUFDO1FBRWxDLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsUUFBUSxJQUFJLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNuRCxJQUFJLENBQUMsS0FBSyxHQUFHLFNBQVMsQ0FBQztZQUV2QixPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDO0lBQ3BCLENBQUM7Q0FDRjtBQXRERCxtQ0FzREMifQ==
@@ -0,0 +1,21 @@
1
+ import type { EnvironmentPermissionsV4, UserPermissionV4 } from '@forestadmin/forestadmin-client';
2
+ export interface PermissionsClientOptions {
3
+ forestServerUrl: string;
4
+ envSecret: string;
5
+ }
6
+ export interface EnvironmentAndUserPermissions {
7
+ environmentPermissions: EnvironmentPermissionsV4;
8
+ users: UserPermissionV4[];
9
+ }
10
+ export interface PermissionsFetcher {
11
+ fetchPermissions(): Promise<EnvironmentAndUserPermissions>;
12
+ }
13
+ export default class PermissionsClient implements PermissionsFetcher {
14
+ private readonly api;
15
+ private readonly options;
16
+ private inFlight;
17
+ constructor(options: PermissionsClientOptions);
18
+ fetchPermissions(): Promise<EnvironmentAndUserPermissions>;
19
+ private doFetchPermissions;
20
+ }
21
+ //# sourceMappingURL=permissions-client.d.ts.map
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
4
+ class PermissionsClient {
5
+ constructor(options) {
6
+ this.inFlight = null;
7
+ this.api = new forestadmin_client_1.ForestHttpApi();
8
+ this.options = options;
9
+ }
10
+ fetchPermissions() {
11
+ if (!this.inFlight) {
12
+ this.inFlight = this.doFetchPermissions().finally(() => {
13
+ this.inFlight = null;
14
+ });
15
+ }
16
+ return this.inFlight;
17
+ }
18
+ async doFetchPermissions() {
19
+ const [environmentPermissions, users] = await Promise.all([
20
+ this.api.getEnvironmentPermissions(this.options),
21
+ this.api.getUsers(this.options),
22
+ ]);
23
+ return { environmentPermissions, users };
24
+ }
25
+ }
26
+ exports.default = PermissionsClient;
27
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVybWlzc2lvbnMtY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Blcm1pc3Npb25zL3Blcm1pc3Npb25zLWNsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUVBLHdFQUFnRTtBQWdCaEUsTUFBcUIsaUJBQWlCO0lBTXBDLFlBQVksT0FBaUM7UUFGckMsYUFBUSxHQUFrRCxJQUFJLENBQUM7UUFHckUsSUFBSSxDQUFDLEdBQUcsR0FBRyxJQUFJLGtDQUFhLEVBQUUsQ0FBQztRQUMvQixJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQztJQUN6QixDQUFDO0lBRUQsZ0JBQWdCO1FBQ2QsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNuQixJQUFJLENBQUMsUUFBUSxHQUFHLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUU7Z0JBQ3JELElBQUksQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDO1lBQ3ZCLENBQUMsQ0FBQyxDQUFDO1FBQ0wsQ0FBQztRQUVELE9BQU8sSUFBSSxDQUFDLFFBQVEsQ0FBQztJQUN2QixDQUFDO0lBRU8sS0FBSyxDQUFDLGtCQUFrQjtRQUM5QixNQUFNLENBQUMsc0JBQXNCLEVBQUUsS0FBSyxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDO1lBQ3hELElBQUksQ0FBQyxHQUFHLENBQUMseUJBQXlCLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQztZQUNoRCxJQUFJLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDO1NBQ2hDLENBQUMsQ0FBQztRQUVILE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxLQUFLLEVBQUUsQ0FBQztJQUMzQyxDQUFDO0NBQ0Y7QUE3QkQsb0NBNkJDIn0=
@@ -0,0 +1,13 @@
1
+ import type PermissionsCache from './permissions-cache';
2
+ import type { PermissionsFetcher } from './permissions-client';
3
+ import type { Logger } from '../ports/logger-port';
4
+ import type ReadModelStore from '../read-model/read-model-store';
5
+ import type { Middleware } from 'koa';
6
+ export interface PermissionsRoutesMiddlewareOptions {
7
+ store: ReadModelStore;
8
+ client: PermissionsFetcher;
9
+ cache: PermissionsCache;
10
+ logger: Logger;
11
+ }
12
+ export default function createPermissionsRoutesMiddleware({ store, client, cache, logger, }: PermissionsRoutesMiddlewareOptions): Middleware;
13
+ //# sourceMappingURL=permissions-routes-middleware.d.ts.map
@@ -0,0 +1,99 @@
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 = createPermissionsRoutesMiddleware;
7
+ const forestadmin_client_1 = require("@forestadmin/forestadmin-client");
8
+ const build_permission_hints_1 = __importDefault(require("./build-permission-hints"));
9
+ const agent_route_helpers_1 = require("../http/agent-route-helpers");
10
+ const bff_http_error_1 = require("../http/bff-http-error");
11
+ const bff_local_errors_1 = require("../http/bff-local-errors");
12
+ const PERMISSIONS_ROUTE = '/agent/v1/permissions';
13
+ const UNAVAILABLE_RETRY_AFTER_SECONDS = 5;
14
+ function resolveCaller(ctx) {
15
+ const principal = ctx.state.principal;
16
+ if (principal)
17
+ return { userId: principal.id };
18
+ const identity = ctx.state.apiKeyIdentity;
19
+ if (identity)
20
+ return { userId: identity.user.id };
21
+ throw (0, bff_http_error_1.unauthorized)('No caller identity for this request');
22
+ }
23
+ function resolveRequestedCollections(ctx, readModel) {
24
+ const raw = ctx.query.collections;
25
+ if (raw === undefined)
26
+ return readModel.getAllowedCollections();
27
+ const value = Array.isArray(raw) ? raw.join(',') : raw;
28
+ const requested = value
29
+ .split(',')
30
+ .map(name => name.trim())
31
+ .filter(name => name !== '');
32
+ return [...new Set(requested.filter(name => readModel.isCollectionAllowed(name)))];
33
+ }
34
+ function findCallerRoleId(users, userId) {
35
+ return users.find(user => user.id === userId)?.roleId;
36
+ }
37
+ async function resolvePermissions({ cache, client, logger, }) {
38
+ let resolved;
39
+ try {
40
+ resolved = await client.fetchPermissions();
41
+ }
42
+ catch (error) {
43
+ logger('Warn', 'Permission resolution failed', {
44
+ cause: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
45
+ });
46
+ const stillFresh = cache.getFresh();
47
+ if (stillFresh)
48
+ return { permissions: stillFresh, fromFreshFetch: false };
49
+ throw (0, bff_local_errors_1.permissionsUnavailable)(UNAVAILABLE_RETRY_AFTER_SECONDS);
50
+ }
51
+ const permissions = {
52
+ actionPermissions: (0, forestadmin_client_1.generateActionsFromPermissions)(resolved.environmentPermissions),
53
+ users: resolved.users,
54
+ };
55
+ cache.set(permissions);
56
+ return { permissions, fromFreshFetch: true };
57
+ }
58
+ async function resolveCallerPermissions({ caller, cache, client, logger, }) {
59
+ const cached = cache.getFresh();
60
+ const cachedRoleId = cached && findCallerRoleId(cached.users, caller.userId);
61
+ if (cached && cachedRoleId !== undefined) {
62
+ return { permissions: cached, roleId: cachedRoleId };
63
+ }
64
+ if (cache.wasRejectedByLatestFetch(caller.userId))
65
+ throw (0, bff_local_errors_1.forestIdentityNotAllowed)();
66
+ const { permissions, fromFreshFetch } = await resolvePermissions({ cache, client, logger });
67
+ const roleId = findCallerRoleId(permissions.users, caller.userId);
68
+ if (roleId === undefined) {
69
+ if (fromFreshFetch)
70
+ cache.rememberRejected(caller.userId);
71
+ throw (0, bff_local_errors_1.forestIdentityNotAllowed)();
72
+ }
73
+ return { permissions, roleId };
74
+ }
75
+ function createPermissionsRoutesMiddleware({ store, client, cache, logger, }) {
76
+ return async function permissionsRoutesMiddleware(ctx, next) {
77
+ if (ctx.path !== PERMISSIONS_ROUTE || ctx.method !== 'GET') {
78
+ await next();
79
+ return;
80
+ }
81
+ const caller = resolveCaller(ctx);
82
+ const { permissions, roleId } = await resolveCallerPermissions({
83
+ caller,
84
+ cache,
85
+ client,
86
+ logger,
87
+ });
88
+ const readModel = await (0, agent_route_helpers_1.resolveReadModel)(store);
89
+ const collections = resolveRequestedCollections(ctx, readModel);
90
+ ctx.status = 200;
91
+ ctx.body = (0, build_permission_hints_1.default)({
92
+ actionPermissions: permissions.actionPermissions,
93
+ roleId,
94
+ readModel,
95
+ collections,
96
+ });
97
+ };
98
+ }
99
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVybWlzc2lvbnMtcm91dGVzLW1pZGRsZXdhcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcGVybWlzc2lvbnMvcGVybWlzc2lvbnMtcm91dGVzLW1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFpSUEsb0RBZ0NDO0FBdEpELHdFQUFpRjtBQUVqRixzRkFBNEQ7QUFDNUQscUVBQStEO0FBQy9ELDJEQUFzRDtBQUN0RCwrREFBNEY7QUFFNUYsTUFBTSxpQkFBaUIsR0FBRyx1QkFBdUIsQ0FBQztBQUNsRCxNQUFNLCtCQUErQixHQUFHLENBQUMsQ0FBQztBQWExQyxTQUFTLGFBQWEsQ0FBQyxHQUFZO0lBQ2pDLE1BQU0sU0FBUyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsU0FBOEMsQ0FBQztJQUUzRSxJQUFJLFNBQVM7UUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLFNBQVMsQ0FBQyxFQUFFLEVBQUUsQ0FBQztJQUUvQyxNQUFNLFFBQVEsR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLGNBQW9ELENBQUM7SUFFaEYsSUFBSSxRQUFRO1FBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsRUFBRSxDQUFDO0lBRWxELE1BQU0sSUFBQSw2QkFBWSxFQUFDLHFDQUFxQyxDQUFDLENBQUM7QUFDNUQsQ0FBQztBQUVELFNBQVMsMkJBQTJCLENBQUMsR0FBWSxFQUFFLFNBQW9CO0lBQ3JFLE1BQU0sR0FBRyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDO0lBRWxDLElBQUksR0FBRyxLQUFLLFNBQVM7UUFBRSxPQUFPLFNBQVMsQ0FBQyxxQkFBcUIsRUFBRSxDQUFDO0lBRWhFLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxD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
@@ -1,12 +1,17 @@
1
1
  import type { CapabilitiesFetcher } from './capabilities-cache';
2
2
  export interface AgentCapabilitiesFetcherOptions {
3
3
  agentUrl: string;
4
- token: string;
4
+ /** A borrowed request token, or a factory when the caller can mint one per fetch. */
5
+ token: string | (() => string);
5
6
  timeoutMs?: number;
6
7
  }
7
8
  /**
8
- * Builds a capabilities fetcher bound to a request's agent token. The cache calls it only on a
9
- * miss, so the token of whichever request first populates a collection is the one used.
9
+ * Builds a capabilities fetcher bound to an agent token. The cache calls it only on a miss, so the
10
+ * token of whichever request first populates a collection is the one used.
11
+ *
12
+ * A factory exists for the token because an agent token lives 5 minutes: a caller fanning out over
13
+ * hundreds of collections can outlive a single one, and the tail of that fan-out would fail on an
14
+ * expired token. A request can only borrow its caller's, so it passes the string.
10
15
  */
11
16
  export default function createAgentCapabilitiesFetcher({ agentUrl, token, timeoutMs, }: AgentCapabilitiesFetcherOptions): CapabilitiesFetcher;
12
17
  //# sourceMappingURL=agent-capabilities-fetcher.d.ts.map
@@ -7,15 +7,23 @@ exports.default = createAgentCapabilitiesFetcher;
7
7
  const agent_client_1 = require("@forestadmin/agent-client");
8
8
  const create_agent_http_requester_1 = __importDefault(require("../agent/create-agent-http-requester"));
9
9
  /**
10
- * Builds a capabilities fetcher bound to a request's agent token. The cache calls it only on a
11
- * miss, so the token of whichever request first populates a collection is the one used.
10
+ * Builds a capabilities fetcher bound to an agent token. The cache calls it only on a miss, so the
11
+ * token of whichever request first populates a collection is the one used.
12
+ *
13
+ * A factory exists for the token because an agent token lives 5 minutes: a caller fanning out over
14
+ * hundreds of collections can outlive a single one, and the tail of that fan-out would fail on an
15
+ * expired token. A request can only borrow its caller's, so it passes the string.
12
16
  */
13
17
  function createAgentCapabilitiesFetcher({ agentUrl, token, timeoutMs, }) {
14
- const client = (0, agent_client_1.createRemoteAgentClient)({
18
+ const clientFor = (bearer) => (0, agent_client_1.createRemoteAgentClient)({
15
19
  url: agentUrl,
16
- token,
17
- httpRequester: (0, create_agent_http_requester_1.default)(token, agentUrl, timeoutMs),
20
+ token: bearer,
21
+ httpRequester: (0, create_agent_http_requester_1.default)(bearer, agentUrl, timeoutMs),
18
22
  });
19
- return collection => client.collection(collection).capabilities();
23
+ if (typeof token === 'string') {
24
+ const client = clientFor(token);
25
+ return collection => client.collection(collection).capabilities();
26
+ }
27
+ return collection => clientFor(token()).collection(collection).capabilities();
20
28
  }
21
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWdlbnQtY2FwYWJpbGl0aWVzLWZldGNoZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcmVhZC1tb2RlbC9hZ2VudC1jYXBhYmlsaXRpZXMtZmV0Y2hlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQWdCQSxpREFZQztBQTFCRCw0REFBb0U7QUFFcEUsdUdBQTRFO0FBUTVFOzs7R0FHRztBQUNILFNBQXdCLDhCQUE4QixDQUFDLEVBQ3JELFFBQVEsRUFDUixLQUFLLEVBQ0wsU0FBUyxHQUN1QjtJQUNoQyxNQUFNLE1BQU0sR0FBRyxJQUFBLHNDQUF1QixFQUFDO1FBQ3JDLEdBQUcsRUFBRSxRQUFRO1FBQ2IsS0FBSztRQUNMLGFBQWEsRUFBRSxJQUFBLHFDQUF3QixFQUFDLEtBQUssRUFBRSxRQUFRLEVBQUUsU0FBUyxDQUFDO0tBQ3BFLENBQUMsQ0FBQztJQUVILE9BQU8sVUFBVSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFVBQVUsQ0FBQyxDQUFDLFlBQVksRUFBRSxDQUFDO0FBQ3BFLENBQUMifQ==
29
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWdlbnQtY2FwYWJpbGl0aWVzLWZldGNoZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcmVhZC1tb2RlbC9hZ2VudC1jYXBhYmlsaXRpZXMtZmV0Y2hlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQXFCQSxpREFtQkM7QUF0Q0QsNERBQW9FO0FBRXBFLHVHQUE0RTtBQVM1RTs7Ozs7OztHQU9HO0FBQ0gsU0FBd0IsOEJBQThCLENBQUMsRUFDckQsUUFBUSxFQUNSLEtBQUssRUFDTCxTQUFTLEdBQ3VCO0lBQ2hDLE1BQU0sU0FBUyxHQUFHLENBQUMsTUFBYyxFQUFFLEVBQUUsQ0FDbkMsSUFBQSxzQ0FBdUIsRUFBQztRQUN0QixHQUFHLEVBQUUsUUFBUTtRQUNiLEtBQUssRUFBRSxNQUFNO1FBQ2IsYUFBYSxFQUFFLElBQUEscUNBQXdCLEVBQUMsTUFBTSxFQUFFLFFBQVEsRUFBRSxTQUFTLENBQUM7S0FDckUsQ0FBQyxDQUFDO0lBRUwsSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUM5QixNQUFNLE1BQU0sR0FBRyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFaEMsT0FBTyxVQUFVLENBQUMsRUFBRSxDQUFDLE1BQU0sQ0FBQyxVQUFVLENBQUMsVUFBVSxDQUFDLENBQUMsWUFBWSxFQUFFLENBQUM7SUFDcEUsQ0FBQztJQUVELE9BQU8sVUFBVSxDQUFDLEVBQUUsQ0FBQyxTQUFTLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQyxVQUFVLENBQUMsVUFBVSxDQUFDLENBQUMsWUFBWSxFQUFFLENBQUM7QUFDaEYsQ0FBQyJ9
@@ -1,7 +1,17 @@
1
+ /**
2
+ * A Forest column type: a primitive name, a one-element array wrapping another type, a composite
3
+ * `{ fields }`, or a relation marker like `ManyToOne`.
4
+ */
5
+ export type FieldType = string | FieldType[] | {
6
+ fields: {
7
+ field: string;
8
+ type: FieldType;
9
+ }[];
10
+ };
1
11
  export interface CapabilitiesResult {
2
12
  fields: {
3
13
  name: string;
4
- type: string;
14
+ type: FieldType;
5
15
  operators?: string[];
6
16
  }[];
7
17
  }
@@ -44,4 +44,4 @@ class CapabilitiesCache {
44
44
  }
45
45
  }
46
46
  exports.default = CapabilitiesCache;
47
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FwYWJpbGl0aWVzLWNhY2hlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlYWQtbW9kZWwvY2FwYWJpbGl0aWVzLWNhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsaURBQTRDO0FBa0I1Qzs7O0dBR0c7QUFDSCxNQUFxQixpQkFBaUI7SUFRcEMsWUFBWSxFQUFFLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssR0FBRyx5QkFBVSxLQUErQixFQUFFO1FBSmhFLFlBQU8sR0FBRyxJQUFJLEdBQUcsRUFBc0IsQ0FBQztRQUN4QyxhQUFRLEdBQUcsSUFBSSxHQUFHLEVBQXVDLENBQUM7UUFDbkUsZUFBVSxHQUFHLENBQUMsQ0FBQztRQUdyQixJQUFJLENBQUMsR0FBRyxHQUFHLEdBQUcsQ0FBQztRQUNmLElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDO0lBQ3JCLENBQUM7SUFFRCxLQUFLLENBQUMsR0FBRyxDQUFDLFVBQWtCLEVBQUUsT0FBNEI7UUFDeEQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0MsSUFBSSxLQUFLLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLEtBQUssQ0FBQyxTQUFTLEdBQUcsSUFBSSxDQUFDLEtBQUs7WUFBRSxPQUFPLEtBQUssQ0FBQyxNQUFNLENBQUM7UUFFNUUsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDOUMsSUFBSSxPQUFPO1lBQUUsT0FBTyxPQUFPLENBQUM7UUFFNUIsTUFBTSxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksQ0FBQztRQUM1QixNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDO2FBQzlCLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRTtZQUNiLHlGQUF5RjtZQUN6RixxRkFBcUY7WUFDckYsSUFBSSxJQUFJLENBQUMsVUFBVSxLQUFLLFVBQVUsRUFBRSxDQUFDO2dCQUNuQyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDbEUsQ0FBQztZQUVELE9BQU8sTUFBTSxDQUFDO1FBQ2hCLENBQUMsQ0FBQzthQUNELE9BQU8sQ0FBQyxHQUFHLEVBQUU7WUFDWixJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxLQUFLLEtBQUs7Z0JBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDaEYsQ0FBQyxDQUFDLENBQUM7UUFFTCxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFFckMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRUQsS0FBSztRQUNILElBQUksQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDO1FBQ3JCLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDckIsSUFBSSxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUN4QixDQUFDO0NBQ0Y7QUE3Q0Qsb0NBNkNDIn0=
47
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FwYWJpbGl0aWVzLWNhY2hlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3JlYWQtbW9kZWwvY2FwYWJpbGl0aWVzLWNhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsaURBQTRDO0FBMEI1Qzs7O0dBR0c7QUFDSCxNQUFxQixpQkFBaUI7SUFRcEMsWUFBWSxFQUFFLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssR0FBRyx5QkFBVSxLQUErQixFQUFFO1FBSmhFLFlBQU8sR0FBRyxJQUFJLEdBQUcsRUFBc0IsQ0FBQztRQUN4QyxhQUFRLEdBQUcsSUFBSSxHQUFHLEVBQXVDLENBQUM7UUFDbkUsZUFBVSxHQUFHLENBQUMsQ0FBQztRQUdyQixJQUFJLENBQUMsR0FBRyxHQUFHLEdBQUcsQ0FBQztRQUNmLElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDO0lBQ3JCLENBQUM7SUFFRCxLQUFLLENBQUMsR0FBRyxDQUFDLFVBQWtCLEVBQUUsT0FBNEI7UUFDeEQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0MsSUFBSSxLQUFLLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLEtBQUssQ0FBQyxTQUFTLEdBQUcsSUFBSSxDQUFDLEtBQUs7WUFBRSxPQUFPLEtBQUssQ0FBQyxNQUFNLENBQUM7UUFFNUUsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDOUMsSUFBSSxPQUFPO1lBQUUsT0FBTyxPQUFPLENBQUM7UUFFNUIsTUFBTSxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksQ0FBQztRQUM1QixNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDO2FBQzlCLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRTtZQUNiLHlGQUF5RjtZQUN6RixxRkFBcUY7WUFDckYsSUFBSSxJQUFJLENBQUMsVUFBVSxLQUFLLFVBQVUsRUFBRSxDQUFDO2dCQUNuQyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDbEUsQ0FBQztZQUVELE9BQU8sTUFBTSxDQUFDO1FBQ2hCLENBQUMsQ0FBQzthQUNELE9BQU8sQ0FBQyxHQUFHLEVBQUU7WUFDWixJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxLQUFLLEtBQUs7Z0JBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDaEYsQ0FBQyxDQUFDLENBQUM7UUFFTCxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFFckMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRUQsS0FBSztRQUNILElBQUksQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDO1FBQ3JCLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDckIsSUFBSSxDQUFDLFFBQVEsQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUN4QixDQUFDO0NBQ0Y7QUE3Q0Qsb0NBNkNDIn0=
@@ -14,6 +14,10 @@ export type PrimaryKeyField = {
14
14
  name: string;
15
15
  type: string;
16
16
  };
17
+ export type ListableRelation = {
18
+ name: string;
19
+ foreignCollection: string;
20
+ };
17
21
  /**
18
22
  * The agent read-model derived from a schema. Names are collection-scoped: relations and actions
19
23
  * are keyed by `(collection, name)` so the same relation/action name on two collections stays
@@ -23,6 +27,7 @@ export type PrimaryKeyField = {
23
27
  export default class ReadModel {
24
28
  private readonly collections;
25
29
  private readonly relations;
30
+ private readonly listableRelations;
26
31
  private readonly actionEndpoints;
27
32
  private readonly primaryKeys;
28
33
  constructor(collections: ForestSchemaCollection[]);
@@ -30,6 +35,16 @@ export default class ReadModel {
30
35
  private buildActionEndpoints;
31
36
  private buildPrimaryKeys;
32
37
  isCollectionAllowed(collection: string): boolean;
38
+ /** A fresh copy per call, so a consumer cannot mutate the shared cached read-model. */
39
+ getAllowedCollections(): string[];
40
+ /**
41
+ * The to-many, non-polymorphic relations of a collection — the only ones with a list/count route.
42
+ * The foreign collection is NOT checked against the allow-list here: the data routes must keep
43
+ * telling a non-listable relation (404 unknown_relation) from a listable one pointing at a hidden
44
+ * collection (404 unknown_collection), so that filter belongs to each caller.
45
+ */
46
+ getListableRelations(collection: string): ListableRelation[];
47
+ getListableRelationTarget(collection: string, relation: string): string | undefined;
33
48
  isRelationAllowed(collection: string, relation: string): boolean;
34
49
  getRelationTarget(collection: string, relation: string): RelationTarget | undefined;
35
50
  isActionAllowed(collection: string, action: string): boolean;