@antelopejs/interface-dms 0.10.4 → 0.13.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 +126 -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 +2 -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 +4 -12
- 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 +133 -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 +9 -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 +157 -152
- package/dist/base/table-view/schema.js +2 -1
- package/dist/base/table-view/schema.js.map +1 -1
- 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/page-layout.d.ts +3 -2
- package/dist/component-options/page-layout.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 +15 -7
- package/dist/db/internal/tenantLifecycle.model.js +16 -7
- package/dist/db/internal/tenantLifecycle.model.js.map +1 -1
- 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/guards.d.ts +1 -1
- package/dist/hooks.d.ts +1 -2
- package/dist/hooks.js +33 -26
- 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/internal/uploads.d.ts +30 -0
- package/dist/internal/uploads.js +19 -0
- package/dist/internal/uploads.js.map +1 -1
- 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 +4 -2
- package/dist/page/internal/metadata-upload-registration.js +13 -4
- package/dist/page/internal/metadata-upload-registration.js.map +1 -1
- package/dist/page/internal/metadata-uploads.d.ts +5 -5
- package/dist/page/internal/metadata-uploads.js +4 -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 +23 -16
- 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 +43 -4
- package/dist/tenant-lifecycle.js +97 -18
- package/dist/tenant-lifecycle.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/uploads.d.ts +1 -1
- package/dist/uploads.js +39 -60
- 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 +150 -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 +79 -51
- 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 +270 -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 +35 -368
- 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 +11 -11
- 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
package/docs/1.core/5.tenancy.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Tenancy"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Schema constants, tenancy tables, request tenant resolution, access gates, membership writes, and the tenant lifecycle contract."
|
|
4
|
+
mode: reference
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Tenancy
|
|
7
|
-
|
|
8
7
|
## Overview
|
|
9
8
|
|
|
10
|
-
The DMS serves several tenants from one backend. Tenant-owned tables live in a per-tenant schema, and a model handle is bound to one tenant when you obtain it
|
|
9
|
+
The DMS serves several tenants from one backend. Tenant-owned tables live in a per-tenant schema, and a model handle is bound to one tenant when you obtain it. `GetModel(TenantMemberModel, tenantId)` reads and writes the rows of that tenant only. No query filters rows by tenant, and no table carries a `tenantId` column. The handle carries the isolation.
|
|
11
10
|
|
|
12
|
-
For a first implementation, follow [Multi-tenant data](/docs/auth-and-tenancy/multi-tenant)
|
|
11
|
+
For a first implementation, follow [Multi-tenant data](/docs/auth-and-tenancy/multi-tenant). It registers the table in the tenant schema, protects the route, and injects a tenant-bound model. This page lists the schema constants and the lifecycle contracts. Resolving a tenant id is not an authorization check, so `getRequestTenantId` must run behind authentication.
|
|
13
12
|
|
|
14
|
-
The tenancy
|
|
13
|
+
The tenancy exports span several subpaths:
|
|
15
14
|
|
|
16
15
|
| Subpath | Contains |
|
|
17
16
|
| ------- | -------- |
|
|
@@ -21,6 +20,7 @@ The tenancy surface spans several subpaths:
|
|
|
21
20
|
| `@antelopejs/interface-dms/tenant-scoped-model` | Injecting a tenant-bound model |
|
|
22
21
|
| `@antelopejs/interface-dms/tenant-access` | Tenant access gates |
|
|
23
22
|
| `@antelopejs/interface-dms/tenant-ownership` | Membership and ownership writes |
|
|
23
|
+
| `@antelopejs/interface-dms/tenant-members` | `changeTenantMembers`, the one write path for memberships and invitations |
|
|
24
24
|
| `@antelopejs/interface-dms/tenant-lifecycle` | Producer admission and permanent closure before deletion |
|
|
25
25
|
| `@antelopejs/interface-dms/utils/saas-mode` | Declaring SaaS mode |
|
|
26
26
|
| `@antelopejs/interface-dms/data-controllers` | The built-in members and roles data controllers |
|
|
@@ -65,36 +65,35 @@ export class Invoice extends Table {
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
## The
|
|
68
|
+
## The tenancy tables
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
`@antelopejs/interface-dms/db` exports the three tables, their models, and the table-name constants `tenantsTableName`, `tenantMembersTableName` and `ROLES_TABLE_NAME`.
|
|
71
71
|
|
|
72
72
|
### `Tenant`
|
|
73
73
|
|
|
74
|
-
Table `tenants` in `dms-core
|
|
74
|
+
Table `tenants` in `dms-core`. It holds one row per tenant, shared across the backend.
|
|
75
75
|
|
|
76
76
|
| Field | Type | Notes |
|
|
77
77
|
| ----- | ---- | ----- |
|
|
78
|
-
| `_id` | `string` | Tenant id.
|
|
78
|
+
| `_id` | `string` | Tenant id. Every per-tenant model handle takes it as the instance id. |
|
|
79
79
|
| `name` | `string` | Display name. |
|
|
80
80
|
| `createdAt` | `Date` | Creation time, indexed. |
|
|
81
81
|
| `updatedAt` | `Date` | Update time, indexed. |
|
|
82
82
|
|
|
83
|
-
`TenantModel` extends `BasicDataModel(Tenant, tenantsTableName)` and adds `getMany(ids: string[]): Promise<Tenant[]>`, which returns `[]` for an empty id list.
|
|
84
|
-
|
|
85
|
-
Being a core-schema table, `TenantModel` is obtained without an instance id: `GetModel(TenantModel)`.
|
|
83
|
+
`TenantModel` extends `BasicDataModel(Tenant, tenantsTableName)` and adds `getMany(ids: string[]): Promise<Tenant[]>`, which returns `[]` for an empty id list. `tenants` is a core-schema table, so you obtain the model without an instance id: `GetModel(TenantModel)`.
|
|
86
84
|
|
|
87
85
|
### `TenantMember`
|
|
88
86
|
|
|
89
|
-
Table `tenant_members` in `dms-tenant
|
|
87
|
+
Table `tenant_members` in `dms-tenant`. It holds one row per member of the tenant the handle is bound to.
|
|
90
88
|
|
|
91
89
|
| Field | Type | Notes |
|
|
92
90
|
| ----- | ---- | ----- |
|
|
93
91
|
| `_id` | `string` | Membership id. |
|
|
94
|
-
| `userId` | `string` |
|
|
92
|
+
| `userId` | `string` | Related to the `User` table of the [Auth](/docs/interfaces/auth/introduction) domain. Indexed across tenant instances. |
|
|
95
93
|
| `roleIds` | `string[]` | Related to `Role`, many. The roles whose grants make up the member's base permission set. |
|
|
96
94
|
| `isTenantOwner` | `boolean` | Whether the member owns the tenant. |
|
|
97
|
-
| `
|
|
95
|
+
| `inviteDeliveryId` | `string`, optional | Ties this membership to the durable invite acceptance that created it. |
|
|
96
|
+
| `joinedAt` | `Date` | Creation time. Indexed across tenant instances. |
|
|
98
97
|
| `invitedBy` | `string \| null` | Indexed, related to `User`. |
|
|
99
98
|
|
|
100
99
|
`TenantMemberModel` extends `BasicDataModel(TenantMember, tenantMembersTableName)`:
|
|
@@ -104,16 +103,16 @@ Table `tenant_members` in `dms-tenant` — one row per member of the tenant the
|
|
|
104
103
|
| `getByUser(userId)` | `Promise<TenantMember \| undefined>` |
|
|
105
104
|
| `existsByUser(userId)` | `Promise<boolean>` |
|
|
106
105
|
| `listAll()` | `Promise<TenantMember[]>` |
|
|
107
|
-
| `listOwners()` | `Promise<TenantMember[]
|
|
106
|
+
| `listOwners()` | `Promise<TenantMember[]>`, the members with `isTenantOwner` set |
|
|
108
107
|
| `listByUserWithTenantIds(userId)` | `Promise<Array<{ tenantId: string; member: TenantMember }>>` |
|
|
109
|
-
| `getOldestByUser(userId)` | `Promise<{ tenantId: string; member: TenantMember } \| undefined
|
|
108
|
+
| `getOldestByUser(userId)` | `Promise<{ tenantId: string; member: TenantMember } \| undefined>`, the row with the earliest `joinedAt` |
|
|
110
109
|
| `countOwnersExcluding(excludedUserIds)` | `Promise<number>` |
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
`listByUserWithTenantIds` and `getOldestByUser` return the tenant each row came from. A lookup that spans tenant instances needs that id.
|
|
113
112
|
|
|
114
113
|
### `Role`
|
|
115
114
|
|
|
116
|
-
Table `roles` in `dms-tenant
|
|
115
|
+
Table `roles` in `dms-tenant`. It holds the roles of the tenant.
|
|
117
116
|
|
|
118
117
|
| Field | Type | Notes |
|
|
119
118
|
| ----- | ---- | ----- |
|
|
@@ -121,13 +120,14 @@ Table `roles` in `dms-tenant` — the tenant's roles.
|
|
|
121
120
|
| `createdAt` | `Date` | Creation time, indexed. |
|
|
122
121
|
| `updatedAt` | `Date` | Update time, indexed. |
|
|
123
122
|
| `name` | `string` | Indexed. |
|
|
123
|
+
| `description` | `string \| null`, optional | What the role is for, shown in the roles editor. Absent on older rows. |
|
|
124
124
|
| `permissions` | `string[]` | Granted permission ids ([Permissions](/docs/interfaces/core/permissions)). |
|
|
125
125
|
|
|
126
126
|
`RoleModel` extends `BasicDataModel(Role, ROLES_TABLE_NAME)` and adds `getByName(name): Promise<Role | undefined>`.
|
|
127
127
|
|
|
128
|
-
## Resolve the
|
|
128
|
+
## Resolve the request's tenant
|
|
129
129
|
|
|
130
|
-
`getRequestTenantId(ctx)` returns the tenant of the request's
|
|
130
|
+
`getRequestTenantId(ctx)` returns the tenant of the request's verified credential. The DMS binds that credential in the auth decorators (`AuthUser`, `AuthRawUser`, `IfAuthUser` with a valid token, `AuthOwnerOnly`, `AuthUserWithPermission`, and the auth a `PageController` installs), in the tenant guards (`AuthTenantMember`, `AuthTenantOwner` and `authenticateTenantRequest`), and in `authenticateRequestUser`. Without a verified credential, such as an anonymous request or an invalid or forged token, it returns `DEFAULT_TENANT_ID`. The DMS never reads a token's tenant before it checks the token's signature, user and session. When the request's authentication failed, or its `Authorization` header changed after authentication, `getRequestTenantId` throws `HTTPResult(401)`.
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
133
|
import {
|
|
@@ -148,11 +148,11 @@ export class WorkspaceController extends Controller("/api/workspace") {
|
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Call it in the handler, after the route's auth decorator ran. A parameter or property provider
|
|
151
|
+
Call it in the handler, after the route's auth decorator ran. A parameter or property provider resolved alongside the auth decorator uses `resolveRequestTenantId(ctx)` instead. That async function waits for the tenant guards and credential checks the route's other providers start, and answers the tenant they verified. Without one, it verifies a bearer token itself and binds its tenant. A request without a verified credential reads as anonymous, and a credential verified on it later answers 401. `TenantScopedModel` uses it. A resolved tenant still authorizes nothing, so check membership and permissions for the operation.
|
|
152
152
|
|
|
153
|
-
## Scope a
|
|
153
|
+
## Scope a model to the request's tenant
|
|
154
154
|
|
|
155
|
-
`TenantScopedModel(ModelClass)` is a parameter and property decorator
|
|
155
|
+
`TenantScopedModel(ModelClass)` is a parameter and property decorator. It injects `GetModel(ModelClass, await resolveRequestTenantId(ctx))`, which is the tenant of the verified credential, or `DEFAULT_TENANT_ID` without one.
|
|
156
156
|
|
|
157
157
|
```typescript
|
|
158
158
|
import { Controller, Get } from "@antelopejs/interface-api";
|
|
@@ -172,9 +172,9 @@ export class RoleController extends Controller("/api/roles") {
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
As a property decorator it fills
|
|
175
|
+
As a property decorator, it fills the `@ModelReference()` field of a data controller:
|
|
176
176
|
|
|
177
|
-
```typescript
|
|
177
|
+
```typescript fragment
|
|
178
178
|
import { ModelReference } from "@antelopejs/interface-data-api/metadata";
|
|
179
179
|
import { RoleModel } from "@antelopejs/interface-dms/db";
|
|
180
180
|
import { TenantScopedModel } from "@antelopejs/interface-dms/tenant-scoped-model";
|
|
@@ -185,13 +185,13 @@ import { TenantScopedModel } from "@antelopejs/interface-dms/tenant-scoped-model
|
|
|
185
185
|
declare model: RoleModel;
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
Use it only for a model whose table lives in `TENANT_SCHEMA_NAME` or another schema with one instance per tenant. For a core-schema table, take the model without an instance id.
|
|
189
189
|
|
|
190
|
-
## Tenant
|
|
190
|
+
## Tenant access gates
|
|
191
191
|
|
|
192
|
-
A gate is a binary
|
|
192
|
+
A gate is a binary check at tenant level. It decides whether the tenant may use the product at all, independently of any permission set. The DMS evaluates the gates at every tenant authorization point: permission guards, the tenant member and owner guards, table view data routes, page and menu visibility, and realtime page access.
|
|
193
193
|
|
|
194
|
-
```typescript
|
|
194
|
+
```typescript fragment
|
|
195
195
|
import { RegisterTenantAccessGate } from "@antelopejs/interface-dms/tenant-access";
|
|
196
196
|
|
|
197
197
|
export async function construct(): Promise<void> {
|
|
@@ -217,26 +217,24 @@ type TenantAccessGateFn = (
|
|
|
217
217
|
) => Promise<TenantAccessResult> | TenantAccessResult;
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
`TenantAccessGateInfo` carries `id`, `gate` and `order`.
|
|
220
|
+
`TenantAccessGateInfo` carries `id`, `gate` and `order`. The interface guarantees these semantics:
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
- Gates run in ascending `order`, ties in registration order; the first denial wins and its `code` is what reaches the client.
|
|
225
|
-
- Registering an id twice replaces the earlier gate rather than adding a second one.
|
|
222
|
+
- Gates run in ascending `order`, and ties run in registration order. The first denial wins, and its `code` reaches the client.
|
|
223
|
+
- Registering an id a second time replaces the earlier gate. It does not add a second gate.
|
|
226
224
|
- A gate that throws fails the request.
|
|
227
225
|
- The registration is removed when the registering module unloads.
|
|
228
|
-
- Register in `construct()
|
|
226
|
+
- Register in `construct()`, so the gate is active before the API serves requests.
|
|
229
227
|
- `userId` is `undefined` for an unauthenticated request.
|
|
230
228
|
|
|
231
|
-
|
|
229
|
+
Three functions evaluate the gates:
|
|
232
230
|
|
|
233
231
|
| Function | Behavior |
|
|
234
232
|
| -------- | -------- |
|
|
235
233
|
| `CheckTenantAccess(userId, tenantId)` | Resolves to the first denial, or `{ allowed: true }`. |
|
|
236
|
-
| `AssertTenantAccess(userId, tenantId)` | Calls `CheckTenantAccess` and throws a `403` whose body is the denying gate
|
|
237
|
-
| `EvaluateTenantAccess(userId, tenantId)` | Server-side only. Resolves to every denial as `{ gateId, code }
|
|
234
|
+
| `AssertTenantAccess(userId, tenantId)` | Calls `CheckTenantAccess` and throws a `403` whose body is the `code` of the denying gate. |
|
|
235
|
+
| `EvaluateTenantAccess(userId, tenantId)` | Server-side only. Resolves to every denial as `TenantAccessDenial[]` (`{ gateId, code }`), in gate order, and to `[]` when no gate denies. A gate that throws rejects the whole evaluation. |
|
|
238
236
|
|
|
239
|
-
```typescript
|
|
237
|
+
```typescript fragment
|
|
240
238
|
import {
|
|
241
239
|
AssertTenantAccess,
|
|
242
240
|
CheckTenantAccess,
|
|
@@ -250,17 +248,17 @@ if (!access.allowed) {
|
|
|
250
248
|
await AssertTenantAccess(user._id, tenantId); // or let it throw
|
|
251
249
|
```
|
|
252
250
|
|
|
253
|
-
`AuthTenantOwner` and `AuthTenantMember` call `AssertTenantAccess` unless the route opts out with `bypassTenantAccessGate` ([Permissions](/docs/interfaces/core/permissions)).
|
|
251
|
+
`AuthTenantOwner` and `AuthTenantMember` call `AssertTenantAccess` unless the route opts out with `bypassTenantAccessGate` ([Permissions](/docs/interfaces/core/permissions)). A table view opts out with the option of the same name in `TableViewOptions`. Routes that must stay reachable for a denied tenant, such as billing or recovery, use these options.
|
|
254
252
|
|
|
255
|
-
`EvaluateTenantAccess`
|
|
253
|
+
`EvaluateTenantAccess` is for a module that must know whether another denial is in place, for example before it lifts its own hold. Identify gates by `gateId`, never by `code`, because two gates may share a code. The result is an observation. It is not an authorization receipt or an atomic snapshot of external state, so revalidate before you act on it.
|
|
256
254
|
|
|
257
|
-
|
|
255
|
+
A gate denies a tenant outright. To shape what the users of a tenant may do, use a permissions resolver.
|
|
258
256
|
|
|
259
|
-
## Memberships and
|
|
257
|
+
## Memberships and ownership
|
|
260
258
|
|
|
261
|
-
`applyTenantOwnership(userModel, userId, tenantId, patch)`
|
|
259
|
+
`applyTenantOwnership(userModel, userId, tenantId, patch)` adds the user to the tenant, or sets the roles and ownership of an existing member.
|
|
262
260
|
|
|
263
|
-
```typescript
|
|
261
|
+
```typescript fragment
|
|
264
262
|
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
265
263
|
import { applyTenantOwnership } from "@antelopejs/interface-dms/tenant-ownership";
|
|
266
264
|
import { UserModel } from "@antelopejs/interface-dms/auth/db";
|
|
@@ -277,9 +275,10 @@ await applyTenantOwnership(GetModel(UserModel), userId, tenantId, {
|
|
|
277
275
|
| ----- | ---- | ----------- |
|
|
278
276
|
| `roleIds` | `string[]` | Roles the member holds after the write. Replaces the previous list. |
|
|
279
277
|
| `isTenantOwner` | `boolean` | Ownership flag after the write. |
|
|
280
|
-
| `invitedBy` | `string \| null
|
|
278
|
+
| `invitedBy` | `string \| null`, optional | Recorded on insert. Ignored when the membership row already exists. |
|
|
279
|
+
| `deliveryId` | `string`, optional | Durable invite delivery. It grants membership without replacing later edits. |
|
|
281
280
|
|
|
282
|
-
`applyTenantOwnership` goes through [`changeTenantMembers`](#changetenantmembers), which fires `Hook.MEMBER_BEING_CHANGED` before any write, so a module can veto it. When the user is not
|
|
281
|
+
`applyTenantOwnership` goes through [`changeTenantMembers`](#changetenantmembers), which fires `Hook.MEMBER_BEING_CHANGED` before any write, so a module can veto it. When the user is not a member yet, it also fires `Hook.MEMBER_BEING_ADDED` before the write and `Hook.MEMBER_ADDED` after it. For an existing member, it updates `roleIds` and `isTenantOwner` and fires neither of those two hooks ([Hooks](/docs/interfaces/core/hooks)).
|
|
283
282
|
|
|
284
283
|
Two helpers keep the legacy platform-owner flag on the user row aligned with tenant ownership:
|
|
285
284
|
|
|
@@ -288,11 +287,11 @@ Two helpers keep the legacy platform-owner flag on the user row aligned with ten
|
|
|
288
287
|
| `syncPlatformOwnerOnTenantOwnerChange` | `(userModel, userId, tenantId, nextIsTenantOwner: boolean) => Promise<void>` |
|
|
289
288
|
| `clearPlatformOwnerOnMemberRemoval` | `(userModel, userId, tenantId, removedMemberWasTenantOwner: boolean) => Promise<void>` |
|
|
290
289
|
|
|
291
|
-
Both mirror the value onto `User.owner
|
|
290
|
+
Both mirror the value onto `User.owner`. Both do nothing outside the default tenant, and nothing when the backend runs in [SaaS mode](#registersaasmode), where `User.owner` means platform ownership. `clearPlatformOwnerOnMemberRemoval` also does nothing when the removed member was not a tenant owner. Call them after a write that changes ownership without `applyTenantOwnership`, which mirrors the flag itself.
|
|
292
291
|
|
|
293
|
-
###
|
|
292
|
+
### `RegisterSaasMode`
|
|
294
293
|
|
|
295
|
-
By default, the owners of the default tenant are the platform owners. In SaaS mode the two are independent
|
|
294
|
+
By default, the owners of the default tenant are the platform owners. In SaaS mode the two are independent. Tenant owners are customers, and platform ownership is managed separately. The SaaS module declares the mode itself with `RegisterSaasMode`. The DMS never infers it from the modules it finds loaded.
|
|
296
295
|
|
|
297
296
|
```typescript
|
|
298
297
|
import { RegisterSaasMode } from "@antelopejs/interface-dms/utils/saas-mode";
|
|
@@ -304,82 +303,142 @@ export async function construct(): Promise<void> {
|
|
|
304
303
|
|
|
305
304
|
While SaaS mode is on:
|
|
306
305
|
|
|
307
|
-
- the default tenant's owner flag
|
|
308
|
-
-
|
|
306
|
+
- `applyTenantOwnership` and the two helpers above no longer mirror the default tenant's owner flag onto `User.owner`.
|
|
307
|
+
- Removing members from the default tenant no longer refuses to remove the last platform owner.
|
|
309
308
|
|
|
310
|
-
The registration is removed when the registering module unloads
|
|
309
|
+
The registration is removed when the registering module unloads. The mode stays on while at least one registration is active. `isSaasMode()` resolves to the current state, and to `false` until a module registers. Both behaviors read the mode on every request, so the SaaS module may register before or after the DMS starts.
|
|
311
310
|
|
|
312
|
-
## Tenant
|
|
311
|
+
## Tenant lifecycle admission
|
|
313
312
|
|
|
314
|
-
Membership and invite producers coordinate with tenant deletion through `@antelopejs/interface-dms/tenant-lifecycle`. This
|
|
313
|
+
Membership and invite producers coordinate with tenant deletion through `@antelopejs/interface-dms/tenant-lifecycle`. This contract orders writes against deletion. It is not an authorization check or a transaction, so authorize the caller separately.
|
|
315
314
|
|
|
316
315
|
### `changeTenantMembers`
|
|
317
316
|
|
|
318
|
-
Every insert, update or delete of a `TenantMemberModel` or `UserInviteModel` row goes through one entry point, `changeTenantMembers` from `@antelopejs/interface-dms/tenant-members`. The DMS uses it for every membership and invitation write it makes:
|
|
317
|
+
Every insert, update or delete of a `TenantMemberModel` or `UserInviteModel` row goes through one entry point, `changeTenantMembers` from `@antelopejs/interface-dms/tenant-members`. The DMS uses it for every membership and invitation write it makes:
|
|
319
318
|
|
|
320
|
-
|
|
319
|
+
- adding a member;
|
|
320
|
+
- changing a member's roles or ownership, from the members table or the owner route;
|
|
321
|
+
- removing a member, and moving the holders of a deleted role;
|
|
322
|
+
- leaving each tenant when an account is deleted;
|
|
323
|
+
- creating, editing, resending, cancelling, expiring and accepting invitations.
|
|
324
|
+
|
|
325
|
+
A module that writes these rows itself must use it too. There is no other supported write path. A raw `GetModel(...).insert`, `update` or `delete` on these rows skips both the barrier and the policy hook.
|
|
326
|
+
|
|
327
|
+
```typescript fragment
|
|
321
328
|
changeTenantMembers<T>(
|
|
322
329
|
tenantId: string,
|
|
323
|
-
plan: () => Promise<
|
|
330
|
+
plan: () => Promise<TenantMembersWrite<T>>,
|
|
324
331
|
): Promise<T>
|
|
332
|
+
|
|
333
|
+
interface TenantMembersWrite<T> {
|
|
334
|
+
changes: MemberChange[];
|
|
335
|
+
write: () => Promise<T>;
|
|
336
|
+
}
|
|
325
337
|
```
|
|
326
338
|
|
|
327
|
-
It runs
|
|
339
|
+
It runs these steps in order:
|
|
328
340
|
|
|
329
|
-
1. `runTenantLifecycleOperation(tenantId, ...)
|
|
330
|
-
2. `plan()`, inside the admission
|
|
331
|
-
3. `Hook.MEMBER_BEING_CHANGED
|
|
332
|
-
4. `write()
|
|
341
|
+
1. `runTenantLifecycleOperation(tenantId, ...)`. While a [barrier](#pausing-admission) is held, or once admission is closed, the call is refused at once, before anything is read.
|
|
342
|
+
2. `plan()`, inside the admission. It reads the current rows and describes the change.
|
|
343
|
+
3. `Hook.MEMBER_BEING_CHANGED`, once per `MemberChange`, with `{ tenantId, ...change }`. A throwing handler refuses the write, and that includes replays of an invitation acceptance.
|
|
344
|
+
4. `write()`. The admission is then released as `runTenantLifecycleOperation` describes.
|
|
333
345
|
|
|
334
346
|
A `MemberChange` has a `kind`:
|
|
335
347
|
|
|
336
348
|
| `kind` | Fields | When |
|
|
337
349
|
| ------ | ------ | ---- |
|
|
338
|
-
| `added` | `userId`, `isTenantOwner`, `roleIds`, `deliveryId?` | A user joins
|
|
339
|
-
| `removed` | `userId`, `isTenantOwner` | A member is removed, or leaves by deleting their account |
|
|
340
|
-
| `roles` | `userId`, `roleIds`, `previousRoleIds` | A member's roles change, including when a deleted role is reassigned |
|
|
341
|
-
| `ownership` | `userId`, `isTenantOwner`, `previousIsTenantOwner` | A member is given or loses the owner role |
|
|
350
|
+
| `added` | `userId`, `isTenantOwner`, `roleIds`, `deliveryId?` | A user joins by direct add, or by an accepted invitation (`deliveryId` set). |
|
|
351
|
+
| `removed` | `userId`, `isTenantOwner` | A member is removed, or leaves by deleting their account. |
|
|
352
|
+
| `roles` | `userId`, `roleIds`, `previousRoleIds` | A member's roles change, including when a deleted role is reassigned. |
|
|
353
|
+
| `ownership` | `userId`, `isTenantOwner`, `previousIsTenantOwner` | A member is given or loses the owner role. |
|
|
342
354
|
|
|
343
|
-
One edit
|
|
355
|
+
One edit that sets both roles and ownership announces two changes. A write that changes no membership, such as an invitation created, edited or retired, passes `changes: []`. The write is still admitted, and invitation creation keeps its own `Hook.INVITE_BEING_CREATED`. Do not call `changeTenantMembers` from inside another `changeTenantMembers` or `runTenantLifecycleOperation` callback, because each call takes its own admission.
|
|
344
356
|
|
|
345
|
-
Deleting an account makes one call per tenant the user belongs to,
|
|
357
|
+
Deleting an account makes one call per tenant the user belongs to, one after the other. A tenant that holds a barrier refuses its call, and the deletion stops there. The account and its remaining memberships stay, and the user can retry once the barrier is released.
|
|
346
358
|
|
|
347
359
|
### `runTenantLifecycleOperation`
|
|
348
360
|
|
|
349
|
-
`runTenantLifecycleOperation<T>(tenantId: string, work: () => Promise<T>): Promise<T>` admits one invocation
|
|
361
|
+
`runTenantLifecycleOperation<T>(tenantId: string, work: () => Promise<T>): Promise<T>` admits one invocation, calls `work`, then releases that admission and returns the result. Closed admission rejects without running `work`. Membership and invitation writes use it through `changeTenantMembers`. Call it directly only for other work that must coordinate with tenant deletion. That work must await every effect and start no detached writer.
|
|
350
362
|
|
|
351
|
-
A successful callback releases its admission. A deliberate
|
|
363
|
+
A successful callback releases its admission. A deliberate refusal, an `HTTPResult` with a 4xx status, also releases its admission and rethrows the original refusal. The release is tried three times, 200 ms and then 400 ms apart, so a short database outage does not block the tenant. If every try fails, the admission stays, the module logs an error that names the tenant and the attempt, and the call rejects. After a refusal it rejects with an `AggregateError` of the refusal and a `TenantLifecycleReleaseError`. After a success it rejects with the `TenantLifecycleReleaseError`. Any other failure retains the admission, because its effects may be indeterminate. A retry is a new invocation and never retires the identity of the failed one. There is no automatic expiry. An operator retires an admission with [`releaseTenantLifecycleAttempt`](#unblocking-a-deletion).
|
|
352
364
|
|
|
353
|
-
|
|
365
|
+
| `TenantLifecycleReleaseError` field | Value |
|
|
366
|
+
| ----------------------------------- | ----- |
|
|
367
|
+
| `tenantId` | The tenant whose deletion stays blocked. |
|
|
368
|
+
| `attemptId` | The admission that stays, as `observeTenantLifecycle` lists it in `activeAttemptIds`. |
|
|
369
|
+
| `cause` | The failure of the last release try. |
|
|
370
|
+
|
|
371
|
+
### Pausing admission
|
|
354
372
|
|
|
355
373
|
A module that must validate a tenant's inventory with no membership writer in flight, such as a plan downgrade, pauses admission instead of closing it:
|
|
356
374
|
|
|
357
375
|
| Function | Behavior |
|
|
358
376
|
| -------- | -------- |
|
|
359
|
-
| `observeTenantLifecycle(tenantId)` | Resolves to `{ revision, barrierId, activeAttemptIds, closed }`. Never retires an attempt. |
|
|
360
|
-
| `pauseTenantLifecycleAdmission(tenantId, barrierId, expectedRevision)` | Publishes the barrier with one compare-and-set on `expectedRevision`. Resolves `false` when the revision moved, a barrier is held or admission is closed. |
|
|
361
|
-
| `resumeTenantLifecycleAdmission(tenantId, barrierId)` | Releases this barrier
|
|
377
|
+
| `observeTenantLifecycle(tenantId)` | Resolves to a `TenantLifecycleObservation`: `{ revision, barrierId, activeAttemptIds, closed }`. Never retires an attempt. |
|
|
378
|
+
| `pauseTenantLifecycleAdmission(tenantId, barrierId, expectedRevision)` | Publishes the barrier with one compare-and-set on `expectedRevision`. Resolves `false` when the revision moved, a barrier is held, or admission is closed. |
|
|
379
|
+
| `resumeTenantLifecycleAdmission(tenantId, barrierId)` | Releases this barrier. With no barrier held, it invalidates the revision so that a delayed `pause` fails. Resolves `false` for the barrier of another owner or for closed admission. |
|
|
380
|
+
|
|
381
|
+
While a barrier is held, `runTenantLifecycleOperation` rejects new work without running it, with an `HTTPResult` 409 (`Tenant lifecycle admission is paused`). The refusal writes nothing and retains no admission, and the caller may retry once the barrier is released. Admitted work keeps running.
|
|
382
|
+
|
|
383
|
+
Follow this order:
|
|
362
384
|
|
|
363
|
-
|
|
385
|
+
1. Observe the revision before you check your own business intent.
|
|
386
|
+
2. Pause with that revision.
|
|
387
|
+
3. Check the intent again. If you abandon it, resume.
|
|
388
|
+
4. Wait for an observation with no active attempt, then validate.
|
|
389
|
+
|
|
390
|
+
A `false` from `pause` needs a new observation, not a blind retry. Use a unique `barrierId` for each phase. Do not observe or pause from inside a `runTenantLifecycleOperation` callback, because the caller would wait for itself.
|
|
364
391
|
|
|
365
392
|
### `closeTenantLifecycleAdmission`
|
|
366
393
|
|
|
367
|
-
`closeTenantLifecycleAdmission(tenantId: string): Promise<void>`
|
|
394
|
+
`closeTenantLifecycleAdmission(tenantId: string): Promise<void>` closes admission permanently, then checks that no admitted operation remains. It rejects if work is active or indeterminate, or if the database cannot confirm the transition. A rejection can leave admission closed. It never authorizes deletion or reopening.
|
|
395
|
+
|
|
396
|
+
Call it before any destructive tenant phase, including deletion hooks. Do not delete business data when closure rejects. Preserve the `tenant_lifecycle` and `invite_resolutions` safety rows after deletion, because dropping the tenant schema or recreating those rows can reopen admission. Writers on mixed versions, and raw model writes that bypass admission, prevent a safe rollout. These table names describe what to preserve. They are not supported raw-model APIs.
|
|
397
|
+
|
|
398
|
+
The [backend services guide](/docs/building/backend-services#tenant-deletion-closes-producer-admission-first) shows where closure fits in a module's deletion flow.
|
|
399
|
+
|
|
400
|
+
### Unblocking a deletion
|
|
401
|
+
|
|
402
|
+
`releaseTenantLifecycleAttempt(tenantId: string, attemptId: string): Promise<boolean>` retires one admission by hand. It is the operator path for a tenant whose `closeTenantLifecycleAdmission` keeps rejecting. It resolves `true` when it retired the admission. It resolves `false`, and writes nothing, when the tenant holds no such admission: the attempt was already retired, or the tenant or attempt id is wrong. Calling it again is safe.
|
|
403
|
+
|
|
404
|
+
Retire only an attempt whose effects you know are over:
|
|
405
|
+
|
|
406
|
+
- An attempt that a `TenantLifecycleReleaseError` names finished or refused, so none of its effects is in flight. Retire it once the database answers again. The error and the log line give both ids.
|
|
407
|
+
- Any other attempt that `observeTenantLifecycle` lists failed with effects that may be indeterminate. Retire it only after you have confirmed that its work left no write in flight, because the deletion can then overtake that write.
|
|
368
408
|
|
|
369
|
-
Call it
|
|
409
|
+
Call it from an operator script or a one-off task, never from a `runTenantLifecycleOperation` callback or on a timer. Then call `closeTenantLifecycleAdmission` again.
|
|
370
410
|
|
|
371
|
-
|
|
411
|
+
```typescript
|
|
412
|
+
import {
|
|
413
|
+
TenantLifecycleReleaseError,
|
|
414
|
+
closeTenantLifecycleAdmission,
|
|
415
|
+
releaseTenantLifecycleAttempt,
|
|
416
|
+
} from "@antelopejs/interface-dms/tenant-lifecycle";
|
|
417
|
+
|
|
418
|
+
export async function unblockDeletion(
|
|
419
|
+
releaseFailure: TenantLifecycleReleaseError,
|
|
420
|
+
): Promise<void> {
|
|
421
|
+
const retired = await releaseTenantLifecycleAttempt(
|
|
422
|
+
releaseFailure.tenantId,
|
|
423
|
+
releaseFailure.attemptId,
|
|
424
|
+
);
|
|
425
|
+
if (!retired) {
|
|
426
|
+
throw new Error(`No admission ${releaseFailure.attemptId} to retire`);
|
|
427
|
+
}
|
|
428
|
+
await closeTenantLifecycleAdmission(releaseFailure.tenantId);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
372
431
|
|
|
373
|
-
## Built-in
|
|
432
|
+
## Built-in data controllers
|
|
374
433
|
|
|
375
|
-
The interface ships the two data controllers the built-in settings screens
|
|
434
|
+
The interface ships the two data controllers behind the built-in settings screens. The DMS module registers both itself, so their routes exist as soon as it is loaded. Reference them, and do not register them again.
|
|
376
435
|
|
|
377
436
|
| Export | Import from | Table | Base path |
|
|
378
437
|
| ------ | ----------- | ----- | --------- |
|
|
379
438
|
| `memberSettingDataAPI` | `@antelopejs/interface-dms/data-controllers` | `TenantMember` | `/api/tables/members` |
|
|
380
439
|
| `roleSettingDataAPI` | `@antelopejs/interface-dms/data-controllers/roles` | `Role` | `/api/tables/roles` |
|
|
381
440
|
|
|
382
|
-
The `/data-controllers` barrel re-exports the members controller only
|
|
441
|
+
The `/data-controllers` barrel re-exports the members controller only. Import the roles controller from its own path.
|
|
383
442
|
|
|
384
443
|
```typescript
|
|
385
444
|
import { roleSettingDataAPI } from "@antelopejs/interface-dms/data-controllers/roles";
|
|
@@ -397,8 +456,13 @@ const reviewerRolesField = {
|
|
|
397
456
|
};
|
|
398
457
|
```
|
|
399
458
|
|
|
400
|
-
Both bind their model with `TenantScopedModel`, so they read and write the request's tenant.
|
|
459
|
+
Both bind their model with `TenantScopedModel`, so they read and write the request's tenant.
|
|
460
|
+
|
|
461
|
+
- `memberSettingDataAPI` exposes `_id`, `userId`, the member's `name`, `email`, `avatar`, `isValidated`, `lastActiveAt` and `twoFactorMethods` joined from the `User` table, the member's `roleIds` as a plain relation onto `roleSettingDataAPI`, `isTenantOwner`, and `joinedAt`. The DMS members page draws the roles with its own role picker, through the component slot of the edit form. The controller mounts every default table route except `new`, `archive` and `restore`.
|
|
462
|
+
- `roleSettingDataAPI` exposes `_id`, `name`, `permissions`, `createdAt` and `updatedAt`, with every default table route.
|
|
463
|
+
|
|
464
|
+
Reference either controller as the relation target of your own columns when you need a member or role picker, or pass one to a `TableView` to render it.
|
|
401
465
|
|
|
402
|
-
## Next
|
|
466
|
+
## Next steps
|
|
403
467
|
|
|
404
468
|
[Invites](/docs/interfaces/core/invites) covers bringing a user into a tenant that has no membership row for them yet.
|