@antelopejs/interface-dms 0.10.3 → 0.12.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/CHANGELOG.md +47 -0
- package/README.md +21 -10
- package/dist/attachments.d.ts +15 -2
- package/dist/attachments.js +14 -1
- package/dist/attachments.js.map +1 -1
- package/dist/auth/db/models/sessions.model.d.ts +7 -5
- package/dist/auth/db/models/sessions.model.js +2 -9
- package/dist/auth/db/models/sessions.model.js.map +1 -1
- package/dist/auth/db/models/userExternalIdentities.model.d.ts +7 -5
- package/dist/auth/db/models/users.model.d.ts +7 -5
- package/dist/auth/db/tables/users.table.d.ts +4 -2
- package/dist/auth/db/tables/users.table.js.map +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/internal/jwt-auth-decorator.d.ts +1 -1
- package/dist/auth/internal/jwt-auth-decorator.js +6 -3
- package/dist/auth/internal/jwt-auth-decorator.js.map +1 -1
- package/dist/auth/internal/request-authenticators.d.ts +12 -0
- package/dist/auth/internal/request-authenticators.js +53 -10
- package/dist/auth/internal/request-authenticators.js.map +1 -1
- package/dist/base/activity-feed.d.ts +34 -32
- package/dist/base/activity-feed.js +2 -0
- package/dist/base/activity-feed.js.map +1 -1
- package/dist/base/banner.d.ts +12 -10
- package/dist/base/banner.js +2 -0
- package/dist/base/banner.js.map +1 -1
- package/dist/base/block-registry/internal/describe.js +3 -9
- package/dist/base/block-registry/internal/describe.js.map +1 -1
- package/dist/base/block-registry/internal/ui.js +1 -1
- package/dist/base/block-registry/internal/ui.js.map +1 -1
- package/dist/base/card.d.ts +11 -9
- package/dist/base/card.js +2 -0
- package/dist/base/card.js.map +1 -1
- package/dist/base/chart-card.d.ts +4 -2
- package/dist/base/chart-card.js +2 -0
- package/dist/base/chart-card.js.map +1 -1
- package/dist/base/chart-schemas.d.ts +148 -148
- package/dist/base/chart.d.ts +65 -0
- package/dist/base/chart.js +65 -0
- package/dist/base/chart.js.map +1 -1
- package/dist/base/code-block.d.ts +5 -3
- package/dist/base/code-block.js +2 -0
- package/dist/base/code-block.js.map +1 -1
- package/dist/base/custom.d.ts +5 -0
- package/dist/base/custom.js +5 -0
- package/dist/base/custom.js.map +1 -1
- package/dist/base/data-types/default/address.d.ts +5 -0
- package/dist/base/data-types/default/address.js +5 -0
- package/dist/base/data-types/default/address.js.map +1 -1
- package/dist/base/data-types/default/choices.d.ts +20 -0
- package/dist/base/data-types/default/choices.js +20 -0
- package/dist/base/data-types/default/choices.js.map +1 -1
- package/dist/base/data-types/default/files.d.ts +22 -5
- package/dist/base/data-types/default/files.js +20 -0
- package/dist/base/data-types/default/files.js.map +1 -1
- package/dist/base/data-types/default/numbers.d.ts +15 -0
- package/dist/base/data-types/default/numbers.js +25 -9
- package/dist/base/data-types/default/numbers.js.map +1 -1
- package/dist/base/data-types/default/relations.d.ts +10 -2
- package/dist/base/data-types/default/relations.js +9 -6
- package/dist/base/data-types/default/relations.js.map +1 -1
- package/dist/base/data-types/default/strings.d.ts +25 -0
- package/dist/base/data-types/default/strings.js +25 -0
- package/dist/base/data-types/default/strings.js.map +1 -1
- package/dist/base/data-types/default-types.d.ts +1 -1
- package/dist/base/data-types/field-types.d.ts +10 -0
- package/dist/base/data-types/field-types.js +10 -0
- package/dist/base/data-types/field-types.js.map +1 -1
- package/dist/base/data-types/internal/relation-endpoints.js +1 -0
- package/dist/base/data-types/internal/relation-endpoints.js.map +1 -1
- package/dist/base/data-types/status-type.d.ts +5 -0
- package/dist/base/data-types/status-type.js +5 -0
- package/dist/base/data-types/status-type.js.map +1 -1
- package/dist/base/display.d.ts +6 -6
- package/dist/base/empty-state.d.ts +15 -13
- package/dist/base/empty-state.js +2 -0
- package/dist/base/empty-state.js.map +1 -1
- package/dist/base/form-block-schema.d.ts +50 -50
- package/dist/base/form-schema.d.ts +2 -0
- package/dist/base/form-schema.js +2 -0
- package/dist/base/form-schema.js.map +1 -1
- package/dist/base/grid.d.ts +4 -0
- package/dist/base/grid.js +4 -0
- package/dist/base/grid.js.map +1 -1
- package/dist/base/guard.d.ts +30 -28
- package/dist/base/guard.js +2 -0
- package/dist/base/guard.js.map +1 -1
- package/dist/base/helpers/archive-mode-helpers.d.ts +1 -1
- package/dist/base/helpers/file-refs.js +15 -15
- package/dist/base/helpers/file-refs.js.map +1 -1
- package/dist/base/internal/form-schema-validation.js +7 -1
- package/dist/base/internal/form-schema-validation.js.map +1 -1
- package/dist/base/internal/realtime-topics.js.map +1 -1
- package/dist/base/internal/searchable.d.ts +2 -1
- package/dist/base/internal/searchable.js +1 -3
- package/dist/base/internal/searchable.js.map +1 -1
- package/dist/base/key-value-list.d.ts +21 -19
- package/dist/base/key-value-list.js +2 -0
- package/dist/base/key-value-list.js.map +1 -1
- package/dist/base/kpi-card.d.ts +4 -2
- package/dist/base/kpi-card.js +2 -0
- package/dist/base/kpi-card.js.map +1 -1
- package/dist/base/live-toggle.d.ts +2 -0
- package/dist/base/live-toggle.js +2 -0
- package/dist/base/live-toggle.js.map +1 -1
- package/dist/base/meter.d.ts +23 -21
- package/dist/base/meter.js +2 -0
- package/dist/base/meter.js.map +1 -1
- package/dist/base/nav-card-grid.d.ts +24 -19
- package/dist/base/nav-card-grid.js +5 -0
- package/dist/base/nav-card-grid.js.map +1 -1
- package/dist/base/page-toolbar.d.ts +2 -0
- package/dist/base/page-toolbar.js +2 -0
- package/dist/base/page-toolbar.js.map +1 -1
- package/dist/base/period-selector.d.ts +3 -1
- package/dist/base/period-selector.js +2 -0
- package/dist/base/period-selector.js.map +1 -1
- package/dist/base/placeholder.d.ts +2 -0
- package/dist/base/placeholder.js +2 -0
- package/dist/base/placeholder.js.map +1 -1
- package/dist/base/resource-form-schema.d.ts +2 -2
- package/dist/base/searchable.d.ts +3 -2
- package/dist/base/searchable.js +2 -2
- package/dist/base/searchable.js.map +1 -1
- package/dist/base/section.d.ts +6 -2
- package/dist/base/section.js +4 -0
- package/dist/base/section.js.map +1 -1
- package/dist/base/stack.d.ts +6 -0
- package/dist/base/stack.js +6 -0
- package/dist/base/stack.js.map +1 -1
- package/dist/base/stat-group.d.ts +16 -14
- package/dist/base/stat-group.js +2 -0
- package/dist/base/stat-group.js.map +1 -1
- package/dist/base/tab.d.ts +10 -8
- package/dist/base/tab.js +2 -0
- package/dist/base/tab.js.map +1 -1
- package/dist/base/table-view/column-display.d.ts +1 -1
- package/dist/base/table-view/column-display.js.map +1 -1
- package/dist/base/table-view/data-functions.d.ts +10 -4
- package/dist/base/table-view/data-functions.js.map +1 -1
- package/dist/base/table-view/factory.d.ts +5 -0
- package/dist/base/table-view/factory.js +8 -4
- package/dist/base/table-view/factory.js.map +1 -1
- package/dist/base/table-view/internal/auth.d.ts +17 -3
- package/dist/base/table-view/internal/auth.js +26 -5
- package/dist/base/table-view/internal/auth.js.map +1 -1
- package/dist/base/table-view/internal/column-conditions.d.ts +8 -1
- package/dist/base/table-view/internal/column-conditions.js +25 -13
- package/dist/base/table-view/internal/column-conditions.js.map +1 -1
- package/dist/base/table-view/internal/column-display.d.ts +1 -2
- package/dist/base/table-view/internal/column-display.js +3 -9
- package/dist/base/table-view/internal/column-display.js.map +1 -1
- package/dist/base/table-view/internal/factory-helpers.d.ts +1 -1
- package/dist/base/table-view/internal/factory-helpers.js +17 -14
- package/dist/base/table-view/internal/factory-helpers.js.map +1 -1
- package/dist/base/table-view/internal/factory-lifecycle-created.d.ts +2 -3
- package/dist/base/table-view/internal/factory-lifecycle-created.js +5 -5
- package/dist/base/table-view/internal/factory-lifecycle-created.js.map +1 -1
- package/dist/base/table-view/internal/factory-lifecycle-filter.d.ts +2 -2
- package/dist/base/table-view/internal/factory-private-actions.d.ts +2 -2
- package/dist/base/table-view/internal/files.d.ts +2 -1
- package/dist/base/table-view/internal/files.js +14 -5
- package/dist/base/table-view/internal/files.js.map +1 -1
- package/dist/base/table-view/internal/footer.d.ts +3 -3
- package/dist/base/table-view/internal/footer.js +16 -15
- package/dist/base/table-view/internal/footer.js.map +1 -1
- package/dist/base/table-view/internal/guards.d.ts +27 -5
- package/dist/base/table-view/internal/guards.js +127 -49
- package/dist/base/table-view/internal/guards.js.map +1 -1
- package/dist/base/table-view/internal/meta-helpers.d.ts +1 -1
- package/dist/base/table-view/internal/meta-helpers.js +3 -4
- package/dist/base/table-view/internal/meta-helpers.js.map +1 -1
- package/dist/base/table-view/internal/meta.js +2 -3
- package/dist/base/table-view/internal/meta.js.map +1 -1
- package/dist/base/table-view/internal/option-contract.d.ts +1 -1
- package/dist/base/table-view/internal/option-footer.d.ts +2 -2
- package/dist/base/table-view/internal/option-row-actions.d.ts +1 -1
- package/dist/base/table-view/internal/realtime.d.ts +52 -11
- package/dist/base/table-view/internal/realtime.js +23 -16
- package/dist/base/table-view/internal/realtime.js.map +1 -1
- package/dist/base/table-view/internal/resource-form.d.ts +1 -1
- package/dist/base/table-view/internal/resource-form.js +1 -1
- package/dist/base/table-view/internal/resource-form.js.map +1 -1
- package/dist/base/table-view/internal/row-actions-schema.d.ts +12 -12
- package/dist/base/table-view/internal/row-rule-shape.d.ts +11 -5
- package/dist/base/table-view/internal/row-rule-shape.js +60 -30
- package/dist/base/table-view/internal/row-rule-shape.js.map +1 -1
- package/dist/base/table-view/internal/row-rules.d.ts +28 -1
- package/dist/base/table-view/internal/row-rules.js +8 -19
- package/dist/base/table-view/internal/row-rules.js.map +1 -1
- package/dist/base/table-view/internal/tabs.d.ts +1 -1
- package/dist/base/table-view/internal/tabs.js +1 -4
- package/dist/base/table-view/internal/tabs.js.map +1 -1
- package/dist/base/table-view/internal/validation.d.ts +2 -2
- package/dist/base/table-view/internal/validation.js +7 -7
- package/dist/base/table-view/internal/validation.js.map +1 -1
- package/dist/base/table-view/meta.d.ts +7 -6
- package/dist/base/table-view/meta.js +12 -7
- package/dist/base/table-view/meta.js.map +1 -1
- package/dist/base/table-view/resource-form.d.ts +3 -1
- package/dist/base/table-view/resource-form.js +2 -0
- package/dist/base/table-view/resource-form.js.map +1 -1
- package/dist/base/table-view/routes.d.ts +73 -21
- package/dist/base/table-view/routes.js +13 -10
- package/dist/base/table-view/routes.js.map +1 -1
- package/dist/base/table-view/schema.d.ts +152 -152
- package/dist/base/table-view/source.js +4 -1
- package/dist/base/table-view/source.js.map +1 -1
- package/dist/base/table-view.d.ts +1 -1
- package/dist/base/table-view.js.map +1 -1
- package/dist/base/top-list-card.d.ts +35 -33
- package/dist/base/top-list-card.js +2 -0
- package/dist/base/top-list-card.js.map +1 -1
- package/dist/base/types/error.js +5 -2
- package/dist/base/types/error.js.map +1 -1
- package/dist/base/types/guards.d.ts +4 -4
- package/dist/base/types/json.d.ts +1 -10
- package/dist/base/types/row-action.d.ts +7 -7
- package/dist/component-options/confirm-dialog.d.ts +2 -0
- package/dist/component-options/confirm-dialog.js.map +1 -1
- package/dist/component-options/data-type.d.ts +2 -2
- package/dist/component-options/data-type.js +7 -7
- package/dist/component-options/data-type.js.map +1 -1
- package/dist/component-options/row-action.d.ts +14 -9
- package/dist/component-slots.d.ts +11 -2
- package/dist/component-slots.js +1 -0
- package/dist/component-slots.js.map +1 -1
- package/dist/component.d.ts +7 -8
- package/dist/component.js +17 -25
- package/dist/component.js.map +1 -1
- package/dist/data-controllers/members.d.ts +11 -7
- package/dist/data-controllers/members.js +1 -1
- package/dist/data-controllers/members.js.map +1 -1
- package/dist/data-controllers/roles.d.ts +32 -10
- package/dist/db/internal/exportJobs.model.d.ts +7 -5
- package/dist/db/internal/inviteResolutions.model.d.ts +7 -5
- package/dist/db/internal/tenantLifecycle.model.d.ts +7 -5
- package/dist/db/models/roles.model.d.ts +7 -5
- package/dist/db/models/tenantMembers.model.d.ts +7 -5
- package/dist/db/models/tenants.model.d.ts +7 -5
- package/dist/db/models/user_invites.model.d.ts +7 -5
- package/dist/db/tables/tenants.table.d.ts +9 -0
- package/dist/db/tables/tenants.table.js +4 -0
- package/dist/db/tables/tenants.table.js.map +1 -1
- package/dist/guards.d.ts +1 -1
- package/dist/hooks.d.ts +10 -9
- package/dist/hooks.js +42 -33
- package/dist/hooks.js.map +1 -1
- package/dist/internal/attachments.d.ts +2 -2
- package/dist/internal/component-builder.js +2 -1
- package/dist/internal/component-builder.js.map +1 -1
- package/dist/internal/component-serialization.d.ts +2 -4
- package/dist/internal/component-serialization.js +4 -3
- package/dist/internal/component-serialization.js.map +1 -1
- package/dist/internal/component-slots.d.ts +19 -2
- package/dist/internal/component-slots.js +35 -12
- package/dist/internal/component-slots.js.map +1 -1
- package/dist/internal/component-watch-filter.d.ts +11 -0
- package/dist/internal/component-watch-filter.js +22 -0
- package/dist/internal/component-watch-filter.js.map +1 -0
- package/dist/internal/component-watches.js +0 -1
- package/dist/internal/component-watches.js.map +1 -1
- package/dist/internal/own-entry.d.ts +7 -0
- package/dist/internal/own-entry.js +13 -0
- package/dist/internal/own-entry.js.map +1 -0
- package/dist/invite-extension-payloads.d.ts +14 -2
- package/dist/invite-extensions/internal/delivery.js +1 -2
- package/dist/invite-extensions/internal/delivery.js.map +1 -1
- package/dist/invite-extensions/internal/edit.d.ts +2 -1
- package/dist/invite-extensions/internal/edit.js +8 -9
- package/dist/invite-extensions/internal/edit.js.map +1 -1
- package/dist/invite-extensions/internal/form-slot.js +8 -15
- package/dist/invite-extensions/internal/form-slot.js.map +1 -1
- package/dist/invite-extensions/internal/payload-slices.d.ts +33 -3
- package/dist/invite-extensions/internal/payload-slices.js +84 -19
- package/dist/invite-extensions/internal/payload-slices.js.map +1 -1
- package/dist/invite-extensions/internal/types.d.ts +10 -1
- package/dist/invite-resolution.js +6 -4
- package/dist/invite-resolution.js.map +1 -1
- package/dist/json.d.ts +10 -0
- package/dist/json.js +3 -0
- package/dist/json.js.map +1 -0
- package/dist/layout-banners.d.ts +5 -2
- package/dist/layout-banners.js.map +1 -1
- package/dist/page/controllers.d.ts +2 -2
- package/dist/page/internal/extension-validation.js +3 -2
- package/dist/page/internal/extension-validation.js.map +1 -1
- package/dist/page/internal/metadata-layout.js +1 -1
- package/dist/page/internal/metadata-layout.js.map +1 -1
- package/dist/page/internal/metadata-publish.js +3 -2
- package/dist/page/internal/metadata-publish.js.map +1 -1
- package/dist/page/internal/metadata-registration.js +5 -4
- package/dist/page/internal/metadata-registration.js.map +1 -1
- package/dist/page/internal/metadata-upload-registration.d.ts +3 -1
- package/dist/page/internal/metadata-upload-registration.js +9 -0
- package/dist/page/internal/metadata-upload-registration.js.map +1 -1
- package/dist/page/internal/metadata-uploads.d.ts +18 -5
- package/dist/page/internal/metadata-uploads.js +7 -8
- package/dist/page/internal/metadata-uploads.js.map +1 -1
- package/dist/page/internal/page-record.d.ts +2 -3
- package/dist/page/internal/page-record.js +19 -14
- package/dist/page/internal/page-record.js.map +1 -1
- package/dist/page/layout-filter.js +17 -6
- package/dist/page/layout-filter.js.map +1 -1
- package/dist/page/metadata.js +5 -7
- package/dist/page/metadata.js.map +1 -1
- package/dist/request-tenant.d.ts +10 -4
- package/dist/request-tenant.js +16 -34
- package/dist/request-tenant.js.map +1 -1
- package/dist/tenant-export.d.ts +2 -1
- package/dist/tenant-lifecycle.d.ts +13 -4
- package/dist/tenant-lifecycle.js +51 -18
- package/dist/tenant-lifecycle.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/uploads.js +42 -59
- package/dist/uploads.js.map +1 -1
- package/dist/utils/fire-and-forget.js.map +1 -1
- package/dist/utils/internal/decorated-class.d.ts +14 -0
- package/dist/utils/internal/decorated-class.js +37 -0
- package/dist/utils/internal/decorated-class.js.map +1 -0
- package/dist/utils/internal/type-check.d.ts +9 -0
- package/dist/utils/internal/type-check.js +13 -5
- package/dist/utils/internal/type-check.js.map +1 -1
- package/dist/utils/internal/value-parser.d.ts +1 -1
- package/dist/utils/internal/value-parser.js +22 -9
- package/dist/utils/internal/value-parser.js.map +1 -1
- package/dist/watch-actions.d.ts +1 -1
- package/docs/1.core/1.introduction.md +82 -77
- package/docs/1.core/10.layout-banners.md +57 -45
- package/docs/1.core/11.modules-and-dynamic-menus.md +178 -0
- package/docs/1.core/12.frontend-modules-and-request-helpers.md +155 -0
- package/docs/1.core/13.tenant-data-export.md +120 -0
- package/docs/1.core/2.pages-and-modules.md +140 -367
- package/docs/1.core/3.components.md +21 -22
- package/docs/1.core/4.permissions.md +92 -71
- package/docs/1.core/5.tenancy.md +112 -86
- package/docs/1.core/6.invites.md +90 -65
- package/docs/1.core/7.hooks.md +96 -166
- package/docs/1.core/8.realtime.md +44 -45
- package/docs/1.core/9.quick-actions.md +37 -30
- package/docs/2.auth/1.introduction.md +33 -35
- package/docs/2.auth/2.authentication.md +60 -38
- package/docs/2.auth/3.tokens-and-sessions.md +56 -55
- package/docs/2.auth/4.users.md +82 -52
- package/docs/2.auth/5.external-identities.md +25 -26
- package/docs/3.base/1.introduction.md +64 -60
- package/docs/3.base/10.display-blocks.md +15 -16
- package/docs/3.base/11.page-layouts.md +273 -0
- package/docs/3.base/12.table-row-actions.md +122 -0
- package/docs/3.base/13.table-filters-and-forms.md +104 -0
- package/docs/3.base/14.table-events-and-helpers.md +114 -0
- package/docs/3.base/15.table-sources-and-gates.md +24 -0
- package/docs/3.base/2.types.md +44 -41
- package/docs/3.base/3.forms.md +27 -28
- package/docs/3.base/4.tables.md +34 -367
- package/docs/3.base/5.charts-and-widgets.md +28 -29
- package/docs/3.base/6.layout.md +21 -270
- package/docs/3.base/8.data-types.md +39 -40
- package/docs/3.base/9.helpers.md +36 -38
- package/docs/4.notifications/1.introduction.md +22 -26
- package/docs/4.notifications/2.categories-and-subjects.md +58 -55
- package/docs/4.notifications/3.sending.md +72 -68
- package/docs/5.html-render/1.introduction.md +119 -81
- package/docs/5.html-render/2.built-in-emails.md +71 -51
- package/docs/index.md +18 -29
- package/docs/tsconfig.json +4 -0
- package/package.json +12 -12
- package/docs/images/emails-admin-invite.png +0 -0
- package/docs/images/emails-change-notice.png +0 -0
- package/docs/images/emails-change-verification.png +0 -0
- package/docs/images/emails-digest-logo-dark.png +0 -0
- package/docs/images/emails-digest-logo-light.png +0 -0
- package/docs/images/emails-digest.png +0 -0
- package/docs/images/emails-export-partial.png +0 -0
- package/docs/images/emails-export-ready.png +0 -0
- package/docs/images/emails-reset-password.png +0 -0
- package/docs/images/emails-two-factor.png +0 -0
- package/docs/images/emails-user-validation.png +0 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Components"
|
|
3
3
|
description: "A component is a serializable description of a piece of UI, not a live object."
|
|
4
|
+
mode: reference
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Components
|
|
7
|
-
|
|
8
7
|
## Overview
|
|
9
8
|
|
|
10
9
|
A component is a serializable description of a piece of UI, not a live object. `ComponentBuilder` collects a component name, an options object, children, watches and actions; `serialize()` turns that into plain JSON-safe data; the page layout route returns the result. Nothing in this interface renders anything.
|
|
11
10
|
|
|
12
11
|
Everything on this page lives in `@antelopejs/interface-dms/component`. The [Base](/docs/interfaces/base/introduction) domain ships the concrete builders (`Form`, `TableView`, `Grid`, the chart family) built on top of these types; use this page when you write a builder of your own or when you need to know what a serialized component looks like.
|
|
13
12
|
|
|
14
|
-
##
|
|
13
|
+
## Serialize, then render
|
|
15
14
|
|
|
16
15
|
Two shapes describe the same component on the two sides of serialization:
|
|
17
16
|
|
|
18
|
-
```typescript
|
|
17
|
+
```typescript fragment
|
|
19
18
|
interface ComponentInfo<T = unknown, C = Component<T>, ChildType = ComponentChild<T, C>> {
|
|
20
19
|
componentName: string;
|
|
21
20
|
options?: T;
|
|
@@ -39,7 +38,7 @@ type ComponentInfoSerialized<T = unknown> = Omit<ComponentInfo<T>, "children"> &
|
|
|
39
38
|
|
|
40
39
|
The extra index signature on a child is the escape hatch for per-child props a container needs (a grid column span, a tab label): whatever you pass in the child metadata survives serialization next to `id`, `component` and `slot`.
|
|
41
40
|
|
|
42
|
-
## Build a
|
|
41
|
+
## Build a component
|
|
43
42
|
|
|
44
43
|
`ComponentBuilder<T>` is the class you instantiate with the frontend component name. Every method returns `this`, so calls chain.
|
|
45
44
|
|
|
@@ -66,14 +65,14 @@ Assign the result to a static field of a page class and `@RegisterPage()` picks
|
|
|
66
65
|
| `mergeOptions(opts)` | Shallow-merge into the existing options. Logs a warning and does nothing when no options were set. |
|
|
67
66
|
| `child(id, component, metadata?)` | Append a child with its own positional permission. `metadata` may carry `slot` plus any additional props. A duplicate `id` logs a warning. |
|
|
68
67
|
| `meta(meta)` | Merge into the component metadata (`name`, `description`, `icon`). |
|
|
69
|
-
| `onCreated(callback)` | Run `callback(
|
|
68
|
+
| `onCreated(callback)` | Run `callback(page)` when the component's page registers. `page` is typed `CreatedPage` (`target`, `pageInfo.fullId`, `pageInfo.fullSlug`); at run time it is the page's `PageMetadata`. |
|
|
70
69
|
| `onFilter(callback)` | Transform the options per caller, from their permission set. |
|
|
71
70
|
| `watch(event, functionId, opts?)` | Declare a watch on this component. |
|
|
72
71
|
| `watchOn(childId, event, functionId, opts?)` | Declare a watch spliced into a child's options at serialization time. |
|
|
73
72
|
| `watchFilter(callback)` | Filter the accumulated watches per caller. |
|
|
74
73
|
| `action(id, definition)` | Declare an `Action` on the component. |
|
|
75
74
|
| `button(id, { permission? })` | Register a named button a quick action can press, with the permission (an action id of this component, or any `Action`) that gates it. A duplicate `id` logs a warning. The table view registers its `customButtons` that carry an `id`. |
|
|
76
|
-
| `navBadge({ count, page? })` | Publish a count as the navigation badge of a page (`NavBadgeSource`): `count(ctx, user)` answers it for the caller on each page visit
|
|
75
|
+
| `navBadge({ count, page? })` | Publish a count as the navigation badge of a page (`NavBadgeSource`): `count(ctx, user)` answers it for the caller on each page visit with a number, drawn neutral, or `{ count, tone }` ([Navigation badges](#navigation-badges)). It throws to publish nothing for a caller who may not read it. `page` names the page controller the badge goes to, the component's own page by default. A table view's tab declared with `navBadge` publishes through it; a component of your own calls it directly (the DMS's notifications, security and roles pages do). A count of zero shows no badge, and a page's static `MenuOptions.badge` wins. |
|
|
77
76
|
| `before(anchor)` | Place the component above `anchor` (a component key of the target page) when injecting it into another page. |
|
|
78
77
|
| `after(anchor)` | Place it below `anchor`. |
|
|
79
78
|
| `order(value)` | Break ties between components landing at the same spot. Defaults to 0. |
|
|
@@ -82,7 +81,7 @@ Assign the result to a static field of a page class and `@RegisterPage()` picks
|
|
|
82
81
|
|
|
83
82
|
`count` answers a number, drawn as a neutral grey pill, or a `NavBadgeCount` that also gives the tone it is drawn in, with the colours of the dashboard's status pills:
|
|
84
83
|
|
|
85
|
-
```typescript
|
|
84
|
+
```typescript fragment
|
|
86
85
|
interface NavBadgeCount {
|
|
87
86
|
count: number;
|
|
88
87
|
tone?: Tone; // from "@antelopejs/interface-dms/base/types/tone"
|
|
@@ -116,11 +115,11 @@ interface ComponentPlacement {
|
|
|
116
115
|
const DEFAULT_PLACEMENT: ComponentPlacement = { side: "end", order: 0 };
|
|
117
116
|
```
|
|
118
117
|
|
|
119
|
-
`anchor` is a component key of the target page: the name of the static field it declared the component under, passed as `.before("table")`, or a dotted path to a nested position, `.before("content.tasks")
|
|
118
|
+
`anchor` is a component key of the target page: the name of the static field it declared the component under, passed as `.before("table")`, or a dotted path to a nested position, `.before("content.tasks")`, stored as the split path. The target page is never imported, so the key is a string. A component that declares no placement gets `DEFAULT_PLACEMENT`: appended after every component of the target page, with no ordering hint. Read the current value off `component.placement`, which is `undefined` until one of the three methods is called.
|
|
120
119
|
|
|
121
120
|
Lower `order` comes first, and equal orders fall back to the extension class name and then to declaration order, so the result never depends on module start order. [Pages and Modules](/docs/interfaces/core/pages-and-modules) covers the extension mechanism these describe placement for.
|
|
122
121
|
|
|
123
|
-
### Children and
|
|
122
|
+
### Children and slots
|
|
124
123
|
|
|
125
124
|
```typescript
|
|
126
125
|
import { ComponentBuilder } from "@antelopejs/interface-dms/component";
|
|
@@ -134,11 +133,11 @@ const panel = new ComponentBuilder<{ title: string }>("acme-panel")
|
|
|
134
133
|
|
|
135
134
|
The `grow: true` above is an arbitrary extra prop: it lands on the serialized child alongside `id`, `component` and `slot`.
|
|
136
135
|
|
|
137
|
-
## The `Component`
|
|
136
|
+
## The `Component` class
|
|
138
137
|
|
|
139
138
|
`ComponentBuilder` extends `Component<T>`, which is also usable directly when you already hold a `ComponentInfo`:
|
|
140
139
|
|
|
141
|
-
```typescript
|
|
140
|
+
```typescript fragment
|
|
142
141
|
new Component<T>(
|
|
143
142
|
componentInfo: ComponentInfoPromise<T>,
|
|
144
143
|
metadata: ComponentMetadata,
|
|
@@ -159,7 +158,7 @@ new Component<T>(
|
|
|
159
158
|
| `onPageCreated` | The callback run when the owning page registers. |
|
|
160
159
|
| `onFilterCallback` | The per-caller options filter, when one is installed. |
|
|
161
160
|
| `placement` | The `ComponentPlacement`, or `undefined` when none was declared. |
|
|
162
|
-
| `serialize()` | `Promise<ComponentInfoSerialized<T
|
|
161
|
+
| `serialize()` | `Promise<ComponentInfoSerialized<T>>`. Awaits the component info and serializes every child. |
|
|
163
162
|
| `serializeSync()` | The same, for a component whose info is not a promise. |
|
|
164
163
|
|
|
165
164
|
Both serialize methods normalize `children` to `[]` when the component has none.
|
|
@@ -168,7 +167,7 @@ Because `componentInfo` may be a promise, a builder can resolve data at registra
|
|
|
168
167
|
|
|
169
168
|
## Actions
|
|
170
169
|
|
|
171
|
-
An action is a named capability of a component that carries its own permission
|
|
170
|
+
An action is a named capability of a component that carries its own permission, such as "add a row", "export" or "restore". Declare one on the builder:
|
|
172
171
|
|
|
173
172
|
```typescript
|
|
174
173
|
import { ComponentBuilder } from "@antelopejs/interface-dms/component";
|
|
@@ -195,13 +194,13 @@ An `Action` instance exposes:
|
|
|
195
194
|
| `permissionId` | The component's permission id with `.<id>` appended, or `undefined` while the component has no permission id yet. |
|
|
196
195
|
| `toPermission()` | A `Permission` built from the definition, or `undefined` when `permissionId` is. |
|
|
197
196
|
|
|
198
|
-
The component's permission id only exists once the owning page has registered, so read `permissionId` from `onCreated` or later
|
|
197
|
+
The component's permission id only exists once the owning page has registered, so read `permissionId` from `onCreated` or later, not while the builder is still being assembled.
|
|
199
198
|
|
|
200
199
|
An `Action` is accepted anywhere a permission is: as the `permission` option of a page ([Pages and Modules](/docs/interfaces/core/pages-and-modules)) and as the target of `AuthUserWithPermission` ([Permissions](/docs/interfaces/core/permissions)).
|
|
201
200
|
|
|
202
201
|
`getPermissionIdRef` is the seam that lets `Action` resolve its component's permission id without importing the page module: it is an object with a single `get(component)` method, replaced by `dms/page` when that module loads. Consumers read `permissionId`; there is no reason to call or reassign the ref.
|
|
203
202
|
|
|
204
|
-
## Per-
|
|
203
|
+
## Per-caller filtering
|
|
205
204
|
|
|
206
205
|
`onFilter` rewrites a component's options for the caller who requested the page layout. It runs during layout resolution, after the component itself passed its permission check.
|
|
207
206
|
|
|
@@ -221,13 +220,13 @@ const list = new ComponentBuilder<ListOptions>("acme-list")
|
|
|
221
220
|
}));
|
|
222
221
|
```
|
|
223
222
|
|
|
224
|
-
The callback receives `(permissions: Set<string>, options: T, permissionId: string, context: ComponentFilterContext)` and returns the options
|
|
223
|
+
The callback receives `(permissions: Set<string>, options: T, permissionId: string, context: ComponentFilterContext)` and returns the options, synchronously or as a promise. The third argument is the component position's own permission id, including the full parent path for a child, so a filter can derive the ids of the actions it needs to test. The fourth is the request the layout is built for: its `tenantId`, its `user` (undefined when unauthenticated), and `query`, the page URL's query when the request carries one (what a header button's `availability` reads; check its values like any input).
|
|
225
224
|
|
|
226
225
|
## Watches
|
|
227
226
|
|
|
228
227
|
A watch binds an event on a component to a registered function. The backend only describes the binding.
|
|
229
228
|
|
|
230
|
-
```typescript
|
|
229
|
+
```typescript fragment
|
|
231
230
|
watch<F extends string>(event: string, functionId: F, opts?: WatchOptions<F>): this
|
|
232
231
|
watchOn<F extends string>(childId: string, event: string, functionId: F, opts?: WatchOptions<F>): this
|
|
233
232
|
```
|
|
@@ -263,7 +262,7 @@ const form = Form({ title: "Profile", fields: [] }).watch(
|
|
|
263
262
|
|
|
264
263
|
`watchFilter(callback)` narrows the watch list per caller: it receives `(permissions, watches, permissionId)` and returns the watches to keep. It runs right after `onFilter` in the same filtering pass, and the `requirePermission` narrowing is applied to whatever it returns.
|
|
265
264
|
|
|
266
|
-
## Component
|
|
265
|
+
## Component events
|
|
267
266
|
|
|
268
267
|
`ComponentEvents` holds the lifecycle event names:
|
|
269
268
|
|
|
@@ -276,12 +275,12 @@ Use them as the `event` argument of `watch` or `watchOn`, or as the event name i
|
|
|
276
275
|
|
|
277
276
|
## `RegisterComponentSlot`
|
|
278
277
|
|
|
279
|
-
`RegisterComponentSlot(slotId: string, resolver: ComponentSlotResolver): () => void`, from `@antelopejs/interface-dms/component-slots`, claims a component slot. The resolver receives the options of each
|
|
278
|
+
`RegisterComponentSlot(slotId: string, resolver: ComponentSlotResolver): () => void`, from `@antelopejs/interface-dms/component-slots`, claims a component slot. The resolver receives the options of each form declaring that `slotId`, once per layout request, and returns the replacement options. Returning the input unchanged contributes nothing. Only a form opens a slot: a `slotId` in the options of another component, or in a form whose options carry no `fields` array, is ignored.
|
|
280
279
|
|
|
281
|
-
`SlotOptions` is `
|
|
280
|
+
`SlotOptions` is the serialized options of a form (`FormPropsSerialized` plus `watchActions`), so `options.fields` needs no cast; `ComponentSlotResolver` is `(options: SlotOptions) => MaybePromise<SlotOptions>`. The latest claim for an id wins. The returned release function removes the claim only if that resolver still owns it, so releasing an older claim does not remove a newer one.
|
|
282
281
|
|
|
283
282
|
Use the returned function when the claiming module stops. [TableView form slots](/docs/components/tables#tableview-options) show how a controller exposes a slot; the controller still owns validation and persistence of contributed fields.
|
|
284
283
|
|
|
285
|
-
## Next
|
|
284
|
+
## Next steps
|
|
286
285
|
|
|
287
286
|
[Permissions](/docs/interfaces/core/permissions) covers the ids these components and actions register, and the guards that check them.
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Permissions"
|
|
3
|
-
description: "Permission ids are
|
|
3
|
+
description: "Permission ids are dot-separated strings built from the page tree. The registry, the permission computation, the resolvers that reshape a set, and the route guards that check it."
|
|
4
|
+
mode: reference
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
A permission id is a dot-separated string built from the page tree: a page's `fullId`, then every component id in the position's path, then the action id when there is one. A role holds a flat list of granted ids, and a platform owner holds the wildcard `*`. `GetUserPermissions` returns the base set a user's roles grant. `GetEffectiveUserPermissions` runs that set through the registered resolvers, and every authorization surface uses the effective set.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Permission ids are hierarchical strings built from the page tree: a page's `fullId`, then every component id in the position's path, then the action id when present. A role holds a flat list of granted ids; a platform owner holds the wildcard `*`. Two functions answer "what can this user do" — `GetUserPermissions` returns the base set from roles, `GetEffectiveUserPermissions` runs that set through the registered resolvers. Authorization surfaces use the effective set.
|
|
11
|
-
|
|
12
|
-
The surface spans three subpaths:
|
|
9
|
+
The API spans three subpaths:
|
|
13
10
|
|
|
14
11
|
| Subpath | Contains |
|
|
15
12
|
| ------- | -------- |
|
|
@@ -17,9 +14,9 @@ The surface spans three subpaths:
|
|
|
17
14
|
| `@antelopejs/interface-dms/permissions-resolver` | Registering and running resolvers |
|
|
18
15
|
| `@antelopejs/interface-dms/guards` | Route guards |
|
|
19
16
|
|
|
20
|
-
## Register a
|
|
17
|
+
## Register a permission
|
|
21
18
|
|
|
22
|
-
Pages, components and actions register their own permissions when the page registers ([Pages and
|
|
19
|
+
Pages, components and actions register their own permissions when the page registers ([Pages and modules](/docs/interfaces/core/pages-and-modules)). Register a standalone permission for a capability that has no component behind it:
|
|
23
20
|
|
|
24
21
|
```typescript
|
|
25
22
|
import { RegisterPermission } from "@antelopejs/interface-dms/permissions";
|
|
@@ -34,6 +31,8 @@ RegisterPermission("reports.export", {
|
|
|
34
31
|
});
|
|
35
32
|
```
|
|
36
33
|
|
|
34
|
+
`RegisterPermission(id, permission)` takes the id twice, once as the registry key and once inside the record. Keep the two equal.
|
|
35
|
+
|
|
37
36
|
### `Permission`
|
|
38
37
|
|
|
39
38
|
| Field | Type | Description |
|
|
@@ -42,16 +41,14 @@ RegisterPermission("reports.export", {
|
|
|
42
41
|
| `title` | `string` | Label shown in the roles editor. Required. |
|
|
43
42
|
| `icon` | `string` | Icon name. |
|
|
44
43
|
| `description` | `string` | Longer description. |
|
|
45
|
-
| `dependencies` | `string[]` | Permission ids this one needs to be of any use. The roles editor grants them with it
|
|
46
|
-
| `defaultGranted` | `boolean` |
|
|
47
|
-
|
|
48
|
-
A table view's actions declare their `dependencies` themselves: `edit` and `export` need `view`, and every action only reached from the rows — `delete`, `archive`, `viewArchived`, `export`, and `add`/`edit`/`view` when their form opens over the table — needs `list`; `restore` needs `viewArchived`. A form opening as its own page and `select` need no `list`.
|
|
44
|
+
| `dependencies` | `string[]` | Permission ids this one needs to be of any use. The roles editor grants them with it and removes it with them, and a role is saved with them and the ids they sit under. `HasPermission` requires each registered dependency, so a missing one makes the permission count for nothing. An unregistered dependency counts as held. |
|
|
45
|
+
| `defaultGranted` | `boolean` | Treats the permission as held by every caller, without a role grant. |
|
|
49
46
|
|
|
50
|
-
`
|
|
47
|
+
A table view's actions declare their own `dependencies`. `edit` and `export` need `view`. Every action reached only from the rows needs `list`: `delete`, `archive`, `viewArchived`, `export`, and `add`, `edit` and `view` when their form opens over the table. `restore` needs `viewArchived`. A form that opens as its own page, and `select`, need no `list`.
|
|
51
48
|
|
|
52
|
-
`UnregisterPermission(id)` drops a permission from the grantable tree
|
|
49
|
+
`UnregisterPermission(id)` drops a permission from the grantable tree. It serves registrations undone while the process keeps running, such as a page extension whose module stopped. A permission left behind after its surface is gone stays offered in the roles editor and stays grantable, so code that registers a permission under a condition unregisters it under the same condition.
|
|
53
50
|
|
|
54
|
-
## Read the
|
|
51
|
+
## Read the registry
|
|
55
52
|
|
|
56
53
|
```typescript
|
|
57
54
|
import {
|
|
@@ -63,25 +60,26 @@ const permission = await GetPermission("reports.export");
|
|
|
63
60
|
const tree = await GetPermissions();
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
`GetPermission(id)` resolves to the `Permission` or `undefined`. `GetPermissions()` resolves to a `Record<string, PermissionTree>` keyed by the first id segment
|
|
63
|
+
`GetPermission(id)` resolves to the `Permission` or `undefined`. `GetPermissions()` resolves to a `Record<string, PermissionTree>` keyed by the first id segment:
|
|
67
64
|
|
|
68
|
-
```typescript
|
|
65
|
+
```typescript fragment
|
|
69
66
|
type PermissionTree = {
|
|
70
67
|
data?: Permission;
|
|
71
68
|
children: Record<string, PermissionTree>;
|
|
72
69
|
};
|
|
73
70
|
```
|
|
74
71
|
|
|
75
|
-
Each level keys its children by the next dot-separated segment. `data` is absent
|
|
72
|
+
Each level keys its children by the next dot-separated segment. `data` is absent on a level that exists only to hold children.
|
|
76
73
|
|
|
77
|
-
Both are interface functions
|
|
74
|
+
Both are interface functions, so they return promises even though the registry is in the same process.
|
|
78
75
|
|
|
79
|
-
## Compute a
|
|
76
|
+
## Compute a user's permissions
|
|
80
77
|
|
|
81
78
|
```typescript
|
|
82
79
|
import {
|
|
83
80
|
GetEffectiveUserPermissions,
|
|
84
81
|
GetUserPermissions,
|
|
82
|
+
HasAnyPermission,
|
|
85
83
|
HasPermission,
|
|
86
84
|
} from "@antelopejs/interface-dms/permissions";
|
|
87
85
|
```
|
|
@@ -89,22 +87,26 @@ import {
|
|
|
89
87
|
| Function | Signature |
|
|
90
88
|
| -------- | --------- |
|
|
91
89
|
| `GetUserPermissions` | `(user: { owner?: boolean }, roleIds: string[], roleModel: RoleModel) => Promise<Set<string>>` |
|
|
92
|
-
| `GetEffectiveUserPermissions` | `(user: { _id: string; owner?: boolean }, tenantId: string, roleIds: string[], roleModel: RoleModel) => Promise<Set<string>>` |
|
|
90
|
+
| `GetEffectiveUserPermissions` | `(user: { _id: string; owner?: boolean }, tenantId: string, roleIds: string[], roleModel: RoleModel, context?: PermissionsResolverContext) => Promise<Set<string>>` |
|
|
93
91
|
| `HasPermission` | `(permissions: Set<string>, permissionId: string) => Promise<boolean>` |
|
|
92
|
+
| `HasAnyPermission` | `(permissions: Set<string>, permissionIds: string[]) => Promise<boolean>` |
|
|
94
93
|
|
|
95
|
-
`GetUserPermissions` returns `Set(["*"])` for a user with `owner: true`, an empty set when `roleIds` is empty, and otherwise the union of the `permissions` arrays of the roles it loads through `roleModel`.
|
|
94
|
+
`GetUserPermissions` returns `Set(["*"])` for a user with `owner: true`, an empty set when `roleIds` is empty, and otherwise the union of the `permissions` arrays of the roles it loads through `roleModel`. That is the base set. It ignores resolvers, so never authorize against it.
|
|
96
95
|
|
|
97
|
-
`GetEffectiveUserPermissions`
|
|
96
|
+
`GetEffectiveUserPermissions` passes the base set through `ApplyPermissionsResolvers`, with `context` (default `{}`) handed to each resolver. Every authorization surface uses this set: route guards, page and menu visibility, table view action checks and search.
|
|
98
97
|
|
|
99
|
-
`HasPermission`
|
|
98
|
+
`HasPermission` checks in this order:
|
|
100
99
|
|
|
101
100
|
1. `true` when the set holds the wildcard `*`.
|
|
102
101
|
2. `false` when the id is module-scoped.
|
|
103
102
|
3. `true` when the registered permission carries `defaultGranted`.
|
|
104
103
|
4. `false` when the set does not hold the id.
|
|
105
|
-
5. `
|
|
104
|
+
5. `false` when the set lacks an id the permission sits under (`pages.sales` and `pages` for `pages.sales.orders`). An id above it that is not a registered permission, or that carries `defaultGranted`, counts as held.
|
|
105
|
+
6. `false` when a registered dependency fails the same check. Otherwise `true`.
|
|
106
|
+
|
|
107
|
+
The wildcard comes first, so a platform owner passes even for a module-scoped id. Step 5 makes a grant worthless without its parents: a role that holds neither a table nor its page is refused the table's `delete` action. The roles editor and the role routes store each grant with every id above it, so only an older saved role, or a set a resolver reshaped, can miss one.
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
`HasAnyPermission` returns `true` when `HasPermission` passes for at least one of the ids.
|
|
108
110
|
|
|
109
111
|
```typescript
|
|
110
112
|
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
@@ -130,11 +132,11 @@ async function canExport(user: User, tenantId: string): Promise<boolean> {
|
|
|
130
132
|
}
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
`RoleModel` and `TenantMemberModel`
|
|
135
|
+
`RoleModel` and `TenantMemberModel` are described in [Tenancy](/docs/interfaces/core/tenancy).
|
|
134
136
|
|
|
135
|
-
## Module-
|
|
137
|
+
## Module-scoped permissions
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
The permissions of a module page or category never enter the grantable permission tree, and a role grant on such an id grants nothing. The ids are marked instead:
|
|
138
140
|
|
|
139
141
|
```typescript
|
|
140
142
|
import {
|
|
@@ -144,19 +146,19 @@ import {
|
|
|
144
146
|
} from "@antelopejs/interface-dms/permissions";
|
|
145
147
|
|
|
146
148
|
MarkModuleScopedPermission("modules.automation.builder");
|
|
147
|
-
IsModuleScopedPermission("modules.automation.builder.canvas.add");
|
|
149
|
+
const scoped = IsModuleScopedPermission("modules.automation.builder.canvas.add");
|
|
148
150
|
UnmarkModuleScopedPermission("modules.automation.builder");
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
`IsModuleScopedPermission(id)`
|
|
153
|
+
`IsModuleScopedPermission(id)` checks the id and each id above it, one dot-separated segment at a time, so marking a page id also covers every component and action id derived from it. In the example, `scoped` is `true`. `UnmarkModuleScopedPermission(id)` removes the mark for that exact id only.
|
|
152
154
|
|
|
153
|
-
Page registration calls `MarkModuleScopedPermission`
|
|
155
|
+
Page and category registration calls `MarkModuleScopedPermission` whenever `isInsideModule` holds ([Modules and dynamic menus](/docs/interfaces/core/modules-and-dynamic-menus#module-urls-and-ids)). Call it directly only for ids you build outside that path.
|
|
154
156
|
|
|
155
|
-
## Reshape the
|
|
157
|
+
## Reshape the effective set
|
|
156
158
|
|
|
157
159
|
A permissions resolver receives a caller's current permission set for a tenant and returns the set to use instead.
|
|
158
160
|
|
|
159
|
-
```typescript
|
|
161
|
+
```typescript fragment
|
|
160
162
|
import { RegisterPermissionsResolver } from "@antelopejs/interface-dms/permissions-resolver";
|
|
161
163
|
|
|
162
164
|
export async function construct(): Promise<void> {
|
|
@@ -180,23 +182,33 @@ type PermissionsResolverFn = (
|
|
|
180
182
|
) => Promise<Set<string>> | Set<string>;
|
|
181
183
|
|
|
182
184
|
interface PermissionsResolverContext {
|
|
183
|
-
/** Set when the call previews a role rather than computing a member's set. */
|
|
184
185
|
preview?: PermissionsResolverPreview;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
interface PermissionsResolverPreview {
|
|
188
|
-
/** The previewed role's permissions, before any resolver reshaped them. */
|
|
189
189
|
rolePermissions: ReadonlySet<string>;
|
|
190
190
|
}
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
`PermissionsResolverInfo` carries `id`, `resolver` and `order`.
|
|
193
|
+
`PermissionsResolverInfo` carries `id`, `resolver` and `order`. `context.preview` is set when the call previews a role instead of computing a member's set, and `rolePermissions` holds the previewed role's permissions before any resolver reshaped them.
|
|
194
|
+
|
|
195
|
+
The interface guarantees these semantics:
|
|
196
|
+
|
|
197
|
+
- Resolvers run in ascending `order`, each one receiving the previous one's result. Equal `order` runs in registration order, which is module load order.
|
|
198
|
+
- Registering an id twice replaces the earlier resolver in its slot instead of adding a second one.
|
|
199
|
+
- A resolver that throws aborts the computation and fails the request. It is never skipped.
|
|
200
|
+
- The registration is removed when the registering module unloads. There is no unregister call.
|
|
201
|
+
- Register in `construct()`, so the resolver is active before the API serves requests.
|
|
202
|
+
|
|
203
|
+
`ApplyPermissionsResolvers(userId, tenantId, basePermissions, context)` runs the pipeline, hands `context` to every resolver, and resolves to the effective set. `GetEffectiveUserPermissions` computes the base set and calls it, so prefer that function.
|
|
204
|
+
|
|
205
|
+
A resolver shapes a permission set. It cannot deny a surface that consults no permission set, and it cannot revoke a `defaultGranted` permission, which `HasPermission` grants after the resolvers ran. To cut a tenant off from the product entirely, register a tenant access gate instead ([Tenancy](/docs/interfaces/core/tenancy)).
|
|
194
206
|
|
|
195
207
|
### Role previews
|
|
196
208
|
|
|
197
|
-
|
|
209
|
+
**Preview as role** in the roles editor runs the resolvers too, over the previewed role's set, and sets `context.preview`. The `userId` of such a call is the person previewing. A resolver that grants or withholds by the user's standing, such as handing an owner every permission of the plan or keeping a screen for owners, checks the flag and shapes the set as for an ordinary member holding only that role:
|
|
198
210
|
|
|
199
|
-
```typescript
|
|
211
|
+
```typescript fragment
|
|
200
212
|
RegisterPermissionsResolver({
|
|
201
213
|
id: "my_app.plan-limits",
|
|
202
214
|
order: 100,
|
|
@@ -210,29 +222,17 @@ RegisterPermissionsResolver({
|
|
|
210
222
|
});
|
|
211
223
|
```
|
|
212
224
|
|
|
213
|
-
The DMS always passes `context
|
|
214
|
-
|
|
215
|
-
Semantics the interface guarantees:
|
|
225
|
+
The DMS always passes `context`, and passes `{}` outside a preview.
|
|
216
226
|
|
|
217
|
-
|
|
218
|
-
- Registering an id twice replaces the earlier resolver rather than adding a second one.
|
|
219
|
-
- A resolver that throws aborts the computation and fails the request. It is never skipped.
|
|
220
|
-
- The registration is removed when the registering module unloads; there is no unregister call.
|
|
221
|
-
- Register in `construct()` so the resolver is active before the API serves requests.
|
|
222
|
-
|
|
223
|
-
`ApplyPermissionsResolvers(userId, tenantId, basePermissions, context?)` runs the pipeline, handing `context` (`{}` when omitted) to every resolver, and resolves to the effective set. Prefer `GetEffectiveUserPermissions`, which computes the base set for you and calls it.
|
|
224
|
-
|
|
225
|
-
A resolver shapes a permission set. It cannot deny a surface that consults no permission set, and it cannot revoke a `defaultGranted` permission, which `HasPermission` grants after the resolvers ran. To cut a tenant off from the product entirely, register a tenant access gate instead ([Tenancy](/docs/interfaces/core/tenancy)).
|
|
226
|
-
|
|
227
|
-
## Guard a Route
|
|
227
|
+
## Guard a route
|
|
228
228
|
|
|
229
229
|
Three decorators from `@antelopejs/interface-dms/guards` gate a route on the caller.
|
|
230
230
|
|
|
231
231
|
### `AuthUserWithPermission`
|
|
232
232
|
|
|
233
|
-
`AuthUserWithPermission(target, options?)` requires an authenticated user
|
|
233
|
+
`AuthUserWithPermission(target, options?)` requires an authenticated user who holds the registered permission of `target`. `target` is a `Component`, a child position, a page controller class, or an `Action` ([Components](/docs/interfaces/core/components)). A child component resolves to its full positional path. `options` is a `TenantGuardOptions`.
|
|
234
234
|
|
|
235
|
-
```typescript
|
|
235
|
+
```typescript fragment
|
|
236
236
|
import { Controller, Get } from "@antelopejs/interface-api";
|
|
237
237
|
import { AuthUserWithPermission } from "@antelopejs/interface-dms/guards";
|
|
238
238
|
import type { User } from "@antelopejs/interface-dms/auth/db";
|
|
@@ -240,17 +240,25 @@ import { InvoicesPage } from "../pages/invoices";
|
|
|
240
240
|
|
|
241
241
|
export class InvoiceReportController extends Controller("/api/invoices") {
|
|
242
242
|
@Get("/summary")
|
|
243
|
-
async summary(@AuthUserWithPermission(InvoicesPage)
|
|
244
|
-
|
|
243
|
+
async summary(@AuthUserWithPermission(InvoicesPage) user: User) {
|
|
244
|
+
return { requestedBy: user._id };
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
The guard
|
|
249
|
+
The guard runs these steps:
|
|
250
|
+
|
|
251
|
+
1. It authenticates the token. A request without a valid user fails authentication.
|
|
252
|
+
2. When the token carries a tenant claim, it runs the tenant access gate, unless `options.bypassTenantAccessGate` is set.
|
|
253
|
+
3. It resolves the target's permission id. A target with no permission id passes with the authenticated user.
|
|
254
|
+
4. When the token carries no tenant claim, it answers `403`, because a permission is granted only inside a tenant.
|
|
255
|
+
5. It loads the caller's membership in that tenant, computes the effective permissions and calls `HasPermission`. A missing permission answers `403` with `Forbidden: missing permission <id>`.
|
|
256
|
+
|
|
257
|
+
`bypassTenantAccessGate` skips step 2 and keeps the permission check.
|
|
250
258
|
|
|
251
259
|
### `AuthTenantOwner` and `AuthTenantMember`
|
|
252
260
|
|
|
253
|
-
|
|
261
|
+
`AuthTenantMember(options?)` requires a member of the request's tenant. `AuthTenantOwner(options?)` also requires `isTenantOwner` on that membership. The tenant comes from the verified credential: the JWT's `tenantId` claim, or `DEFAULT_TENANT_ID` (`"default"`) when the claim is empty. Each guard answers `401` without a valid token and `403` when the requirement fails, with the body `error.tenant_member_required` or `error.tenant_owner_required`. Each then runs the tenant access gate before it returns the user.
|
|
254
262
|
|
|
255
263
|
```typescript
|
|
256
264
|
import { Controller, Post } from "@antelopejs/interface-api";
|
|
@@ -265,29 +273,42 @@ export class BillingController extends Controller("/api/billing") {
|
|
|
265
273
|
async portalSession(
|
|
266
274
|
@AuthTenantOwner({ bypassTenantAccessGate: true }) user: User,
|
|
267
275
|
) {
|
|
268
|
-
|
|
276
|
+
return { owner: user._id };
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
@Post("/usage")
|
|
272
280
|
async usage(@AuthTenantMember() user: User) {
|
|
273
|
-
|
|
281
|
+
return { member: user._id };
|
|
274
282
|
}
|
|
275
283
|
}
|
|
276
284
|
```
|
|
277
285
|
|
|
286
|
+
The portal route stays reachable for a tenant that a gate denies. The usage route admits any member of the request's tenant.
|
|
287
|
+
|
|
278
288
|
`TenantGuardOptions` has one field, and all three guards accept it:
|
|
279
289
|
|
|
280
290
|
| Option | Type | Description |
|
|
281
291
|
| ------ | ---- | ----------- |
|
|
282
|
-
| `bypassTenantAccessGate` | `boolean` |
|
|
292
|
+
| `bypassTenantAccessGate` | `boolean` | Skips the tenant access gate for this route. Reserved for routes that must stay reachable when a gate denies the tenant, such as billing status, a payment portal or invoices. |
|
|
293
|
+
|
|
294
|
+
The two tenant guards accept `TenantRequestGuardOptions`, which extends it:
|
|
295
|
+
|
|
296
|
+
| Option | Type | Description |
|
|
297
|
+
| ------ | ---- | ----------- |
|
|
298
|
+
| `authenticators` | `readonly RequestAuthenticator[]` | Credential handlers this route accepts before the JWT fallback. The handler whose `recognizes(token)` returns `true` authenticates the request. Two handlers that recognize the same token answer `401`. |
|
|
299
|
+
| `requireOwner` | `boolean` | Requires tenant ownership, not platform ownership. `AuthTenantOwner` sets it. |
|
|
300
|
+
|
|
301
|
+
`RequestAuthenticator` comes from `@antelopejs/interface-dms/auth` ([Authentication](/docs/interfaces/auth/introduction)).
|
|
302
|
+
|
|
303
|
+
`authenticateTenantRequest(ctx, options?)` runs the same check as a function and resolves to the `User`. It authenticates the credential again, and checks membership and the tenant gate, on every call, so a long-lived request can call it repeatedly. Product permissions stay the caller's job.
|
|
283
304
|
|
|
284
|
-
The opt-out has two mirrors
|
|
305
|
+
The opt-out has two mirrors for the surfaces a route guard does not cover. `MenuOptions.bypassTenantAccessGate` keeps a page and its menu entry reachable ([Pages and modules](/docs/interfaces/core/pages-and-modules#reach-a-page-behind-a-tenant-gate)). `TableViewOptions.bypassTenantAccessGate` covers a table view's data routes, which belong to a controller and not to any one page.
|
|
285
306
|
|
|
286
|
-
A denial from a gate
|
|
307
|
+
A denial from a gate answers `403` with the gate's `code` as its body ([Tenancy](/docs/interfaces/core/tenancy)).
|
|
287
308
|
|
|
288
|
-
### Guards as
|
|
309
|
+
### Guards as properties
|
|
289
310
|
|
|
290
|
-
All three are parameter
|
|
311
|
+
All three guards are parameter and property decorators. Declared as a class property, a guard gates every route on that controller:
|
|
291
312
|
|
|
292
313
|
```typescript
|
|
293
314
|
import { Controller, Post } from "@antelopejs/interface-api";
|
|
@@ -299,14 +320,14 @@ export class FilesController extends Controller("/api/files") {
|
|
|
299
320
|
declare user: User;
|
|
300
321
|
|
|
301
322
|
@Post("/presign")
|
|
302
|
-
async presign() {
|
|
303
|
-
|
|
323
|
+
async presign(): Promise<{ ok: true }> {
|
|
324
|
+
return { ok: true };
|
|
304
325
|
}
|
|
305
326
|
}
|
|
306
327
|
```
|
|
307
328
|
|
|
308
|
-
|
|
329
|
+
The property provider runs on every request to a route of the controller, whether or not the handler reads the property. The `presign` route above never reads `this.user`, and the guard still runs. That makes the property form a guard: removing the read does not remove the check.
|
|
309
330
|
|
|
310
|
-
## Next
|
|
331
|
+
## Next steps
|
|
311
332
|
|
|
312
333
|
[Tenancy](/docs/interfaces/core/tenancy) covers the tenant the guards resolve, the roles the permission sets come from, and the access gates.
|