@antelopejs/interface-dms 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +70 -68
  3. package/dist/auth/db/models/sessions.model.d.ts +1 -0
  4. package/dist/auth/db/models/userExternalIdentities.model.d.ts +1 -0
  5. package/dist/auth/db/models/users.model.d.ts +1 -0
  6. package/dist/base/block-types.d.ts +1 -0
  7. package/dist/base/block-types.js +1 -0
  8. package/dist/base/block-types.js.map +1 -1
  9. package/dist/base/live-toggle.d.ts +2 -2
  10. package/dist/base/period-selector.d.ts +2 -2
  11. package/dist/base/table-view/schema.d.ts +4 -4
  12. package/dist/db/internal/exportJobs.model.d.ts +1 -0
  13. package/dist/db/internal/inviteResolutions.model.d.ts +1 -0
  14. package/dist/db/internal/tenantLifecycle.model.d.ts +1 -0
  15. package/dist/db/models/roles.model.d.ts +1 -0
  16. package/dist/db/models/tenantMembers.model.d.ts +1 -0
  17. package/dist/db/models/tenants.model.d.ts +1 -0
  18. package/dist/db/models/user_invites.model.d.ts +1 -0
  19. package/docs/.navigation.yml +2 -0
  20. package/docs/1.core/.navigation.yml +2 -0
  21. package/docs/1.core/1.introduction.md +169 -0
  22. package/docs/1.core/10.layout-banners.md +133 -0
  23. package/docs/1.core/2.pages-and-modules.md +647 -0
  24. package/docs/1.core/3.components.md +287 -0
  25. package/docs/1.core/4.permissions.md +279 -0
  26. package/docs/1.core/5.tenancy.md +356 -0
  27. package/docs/1.core/6.invites.md +239 -0
  28. package/docs/1.core/7.hooks.md +354 -0
  29. package/docs/1.core/8.realtime.md +147 -0
  30. package/docs/1.core/9.quick-actions.md +115 -0
  31. package/docs/2.auth/.navigation.yml +2 -0
  32. package/docs/2.auth/1.introduction.md +75 -0
  33. package/docs/2.auth/2.authentication.md +222 -0
  34. package/docs/2.auth/3.tokens-and-sessions.md +352 -0
  35. package/docs/2.auth/4.users.md +256 -0
  36. package/docs/2.auth/5.external-identities.md +122 -0
  37. package/docs/3.base/.navigation.yml +2 -0
  38. package/docs/3.base/1.introduction.md +202 -0
  39. package/docs/3.base/10.display-blocks.md +136 -0
  40. package/docs/3.base/2.types.md +402 -0
  41. package/docs/3.base/3.forms.md +349 -0
  42. package/docs/3.base/4.tables.md +608 -0
  43. package/docs/3.base/5.charts-and-widgets.md +466 -0
  44. package/docs/3.base/6.layout.md +431 -0
  45. package/docs/3.base/8.data-types.md +361 -0
  46. package/docs/3.base/9.helpers.md +414 -0
  47. package/docs/4.notifications/.navigation.yml +2 -0
  48. package/docs/4.notifications/1.introduction.md +79 -0
  49. package/docs/4.notifications/2.categories-and-subjects.md +212 -0
  50. package/docs/4.notifications/3.sending.md +283 -0
  51. package/docs/5.html-render/.navigation.yml +2 -0
  52. package/docs/5.html-render/1.introduction.md +372 -0
  53. package/docs/5.html-render/2.built-in-emails.md +107 -0
  54. package/docs/index.md +100 -0
  55. package/docs/tsconfig.json +11 -0
  56. package/package.json +24 -18
@@ -0,0 +1,256 @@
1
+ ---
2
+ title: "Users"
3
+ description: "The `User` table is the identity record every other part of this interface points at: the authentication decorators resolve a request to one, the token generators sign for one, and `Session` rows reference one."
4
+ ---
5
+
6
+ # Users
7
+
8
+ ## Overview
9
+
10
+ The `User` table is the identity record every other part of this interface points at: the authentication decorators resolve a request to one, the token generators sign for one, and `Session` rows reference one. The table, its constant and its model live on the `/db` subpath; the sanitizer and the mail entry points live on the root subpath.
11
+
12
+ ```typescript
13
+ import { sanitizeUser } from "@antelopejs/interface-dms/auth";
14
+ import {
15
+ USERS_TABLE_NAME,
16
+ User,
17
+ UserModel,
18
+ } from "@antelopejs/interface-dms/auth/db";
19
+ ```
20
+
21
+ ## The User Table
22
+
23
+ `User` extends `Table.with(HashModifier)` and is registered as the `users` table under the DMS core schema, so its rows are shared across tenants and its model is retrieved without a tenant instance. `USERS_TABLE_NAME` holds the table name, `"users"`.
24
+
25
+ | Field | Type | Description |
26
+ | ----- | ---- | ----------- |
27
+ | `_id` | `string` | Primary key. |
28
+ | `createdAt` | `Date` | Set on insert by the creation-time decorator. Indexed. |
29
+ | `updatedAt` | `Date` | Set on write by the update-time decorator. Indexed. |
30
+ | `email` | `string` | Login identifier. Indexed. |
31
+ | `name` | `string` | Display name. |
32
+ | `avatar` | `DefaultDataTypes.ImageValue \| null` | Profile picture as an image value (`{ key, alt? }`) in the default storage, downscaled client-side to a square of at most 100×100 pixels. |
33
+ | `language` | `string` | Preferred language or locale, for example `"en"` or `"fr"`. Indexed. |
34
+ | `password` | `string \| null` | Hashed by the `HashModifier`; reads back as the hash, not the plaintext. `null` for accounts with no password. |
35
+ | `authKey` | `string` | Per-user secret that takes part in JWT signing. Rotating it invalidates every token already issued to the user. |
36
+ | `isValidated` | `boolean` | Whether the email address has been validated. Indexed. |
37
+ | `validationToken` | `string \| null` | Current email-validation code. Indexed. |
38
+ | `validationRequestedAt` | `Date \| null` | When the validation code was issued, for expiry and rate limiting. |
39
+ | `forgotPasswordToken` | `string \| null` | Current password-reset code. Indexed. |
40
+ | `forgotPasswordRequestedAt` | `Date \| null` | When the reset code was issued, for expiry and rate limiting. |
41
+ | `owner` | `boolean` | Platform owner flag. This is what `AuthOwnerOnly` checks — see [Authentication](/docs/interfaces/auth/authentication). |
42
+ | `twoFactorMethods` | `string[]` | Second factors enabled on the account. |
43
+ | `twoFactorSecret` | `string \| null` | Active TOTP secret. |
44
+ | `twoFactorPendingSecret` | `string \| null` | TOTP secret awaiting confirmation during enrolment. |
45
+ | `twoFactorTotpLastStep` | `number \| null` | 30-second time step of the last TOTP code accepted; a code from that step or an earlier one is refused, so each code works once. Unset until one is accepted. |
46
+ | `twoFactorBackupCodes` | `string[]` | Remaining single-use backup codes. |
47
+ | `twoFactorEmailCode` | `string \| null` | Hash of the current emailed second-factor code — hex SHA-256 of `JSON.stringify([userId, code])` — never the code itself. |
48
+ | `twoFactorEmailCodeRequestedAt` | `Date \| null` | When the emailed code was issued, for expiry and rate limiting. |
49
+
50
+ The indexed fields — `createdAt`, `updatedAt`, `email`, `language`, `isValidated`, `validationToken`, `forgotPasswordToken` — are the ones you can look up through `getAll` on the underlying table or `getBy` on the model, which is how `UserModel.getByEmail` works.
51
+
52
+ ## Store and Check a Password
53
+
54
+ `password` carries the `@Hashed` decorator, and the table composes `HashModifier`. Assign a plaintext value on write and it is hashed on the way into the database; read it back and you get the hash, not the original.
55
+
56
+ ```typescript
57
+ import type { UserModel } from "@antelopejs/interface-dms/auth/db";
58
+ import randomstring from "randomstring";
59
+
60
+ const AUTH_KEY_LENGTH = 64;
61
+
62
+ export async function createUser(
63
+ userModel: UserModel,
64
+ name: string,
65
+ email: string,
66
+ password: string,
67
+ ) {
68
+ const ids = await userModel.insert({
69
+ createdAt: new Date(),
70
+ updatedAt: new Date(),
71
+ name,
72
+ email: email.toLowerCase(),
73
+ password, // hashed by the modifier on insert
74
+ authKey: randomstring.generate({ length: AUTH_KEY_LENGTH }),
75
+ owner: false,
76
+ language: "en",
77
+ });
78
+ return ids[0];
79
+ }
80
+ ```
81
+
82
+ To check a candidate password, call `testHash` on the loaded row. The mixin hashes the candidate with the stored salt and compares it against the stored hash.
83
+
84
+ ```typescript
85
+ import { HTTPResult } from "@antelopejs/interface-api";
86
+ import type { UserModel } from "@antelopejs/interface-dms/auth/db";
87
+
88
+ const UNAUTHORIZED = 401;
89
+
90
+ export async function checkCredentials(
91
+ userModel: UserModel,
92
+ email: string,
93
+ password: string,
94
+ ) {
95
+ const user = await userModel.getByEmail(email.toLowerCase());
96
+ if (!user || !user.testHash("password", password)) {
97
+ throw new HTTPResult(UNAUTHORIZED, "error.invalid_credentials");
98
+ }
99
+ return user;
100
+ }
101
+ ```
102
+
103
+ Emails are stored trimmed and lowercased. `getByEmail` normalizes its argument itself; normalize with `normalizeEmail` (exported alongside `UserModel`) before writing one.
104
+
105
+ ## Query Users with UserModel
106
+
107
+ `UserModel` extends `BasicDataModel(User, USERS_TABLE_NAME)`, so it has the standard `get`, `getBy`, `getAll`, `insert`, `update` and `delete` operations, plus three lookups of its own.
108
+
109
+ | Method | Signature | Description |
110
+ | ------ | --------- | ----------- |
111
+ | `getByEmail` | `(email: string) => Promise<User \| undefined>` | The user with this email, trimmed and lowercased before the lookup through the `email` index, or `undefined`. |
112
+ | `getOwners` | `() => Promise<User[]>` | Every user with `owner === true`. |
113
+ | `countOwnersExcluding` | `(excludedIds: string[]) => Promise<number>` | How many owners remain once the given user ids are set aside. Pass an empty array to count all owners. |
114
+
115
+ `countOwnersExcluding` exists for the last-owner check: before removing the owner flag from a set of users, count who would be left and refuse the operation if the answer is zero.
116
+
117
+ ```typescript
118
+ import { HTTPResult } from "@antelopejs/interface-api";
119
+ import type { UserModel } from "@antelopejs/interface-dms/auth/db";
120
+
121
+ const CONFLICT = 409;
122
+
123
+ export async function demoteOwners(userModel: UserModel, userIds: string[]) {
124
+ const remaining = await userModel.countOwnersExcluding(userIds);
125
+ if (remaining === 0) {
126
+ throw new HTTPResult(CONFLICT, "error.last_owner");
127
+ }
128
+
129
+ for (const id of userIds) {
130
+ await userModel.update(id, { owner: false });
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## Invalidate Every Token for a User
136
+
137
+ `authKey` takes part in the signing secret of every token issued for the user, so replacing it makes all outstanding access, refresh, two-factor and tenant-assignment tokens fail validation at once. Combine it with `SessionModel.deleteByUserId` from [Tokens and Sessions](/docs/interfaces/auth/tokens-and-sessions) to sign a user out everywhere.
138
+
139
+ ```typescript
140
+ import type {
141
+ SessionModel,
142
+ User,
143
+ UserModel,
144
+ } from "@antelopejs/interface-dms/auth/db";
145
+ import randomstring from "randomstring";
146
+
147
+ const AUTH_KEY_LENGTH = 64;
148
+
149
+ export async function signOutEverywhere(
150
+ userModel: UserModel,
151
+ sessionModel: SessionModel,
152
+ user: User,
153
+ ) {
154
+ await sessionModel.deleteByUserId(user._id);
155
+ user.authKey = randomstring.generate({ length: AUTH_KEY_LENGTH });
156
+ await userModel.update(user);
157
+ }
158
+ ```
159
+
160
+ `userModel.update(user)` takes the primary key from the object; the two-argument form `update(id, partial)` updates a subset of fields instead.
161
+
162
+ ## Strip Secrets Before Responding
163
+
164
+ A `User` loaded from the database carries authentication secrets. `sanitizeUser` returns a copy without them, and is what you return over HTTP.
165
+
166
+ ```typescript
167
+ sanitizeUser(user: User): Promise<Partial<User>>
168
+ ```
169
+
170
+ It removes the account's secret material — the `authKey`, the email-validation and password-reset tokens with their timestamps, and the two-factor secret, the secret of an enrolment in progress, backup codes and email code with its timestamp — along with any additional keys the deployment lists in its authentication configuration. Because the removed set is configurable, the return type is `Partial<User>`: treat every field as possibly absent rather than assuming a fixed shape.
171
+
172
+ ```typescript
173
+ import { Controller, Get } from "@antelopejs/interface-api";
174
+ import {
175
+ AuthRawUser,
176
+ sanitizeUser,
177
+ } from "@antelopejs/interface-dms/auth";
178
+ import type { User } from "@antelopejs/interface-dms/auth/db";
179
+
180
+ export class MeController extends Controller("/api/me") {
181
+ @Get("")
182
+ me(@AuthRawUser() user: User): Promise<Partial<User>> {
183
+ return sanitizeUser(user);
184
+ }
185
+ }
186
+ ```
187
+
188
+ ## Send Account Emails
189
+
190
+ Four entry points send the account emails the authentication flows depend on. Each returns a promise that resolves once the message has been accepted and rejects when the mail provider reports a failure, so decide per call site whether to await it or dispatch it in the background.
191
+
192
+ | Function | Signature | Sends |
193
+ | -------- | --------- | ----- |
194
+ | `send2FAEmail` | `(user: User, code: string) => Promise<void>` | The emailed second-factor code, to `user.email`. |
195
+ | `sendEmailValidationEmail` | `(user: User) => Promise<void>` | The email-validation message, using the `validationToken` currently stored on the user. |
196
+ | `sendEmailForgotEmail` | `(user: User) => Promise<void>` | The password-reset message, using the `forgotPasswordToken` currently stored on the user. |
197
+ | `sendAdminInviteEmail` | `(email: string, token: string, inviteeName?: string) => Promise<void>` | An invite carrying a signup link built from `token`, to an address that has no user row yet. `inviteeName` addresses the invitee by name when the invite carried one. |
198
+
199
+ `sendEmailValidationEmail` and `sendEmailForgotEmail` read the code off the user, so generate it, persist it, and only then send.
200
+
201
+ ```typescript
202
+ import { sendEmailForgotEmail } from "@antelopejs/interface-dms/auth";
203
+ import type { UserModel } from "@antelopejs/interface-dms/auth/db";
204
+ import randomstring from "randomstring";
205
+
206
+ const RESET_CODE_LENGTH = 6;
207
+
208
+ export async function requestPasswordReset(
209
+ userModel: UserModel,
210
+ email: string,
211
+ ) {
212
+ const user = await userModel.getByEmail(email.toLowerCase());
213
+ if (!user) return; // answer identically whether or not the address exists
214
+
215
+ user.forgotPasswordToken = randomstring.generate({
216
+ length: RESET_CODE_LENGTH,
217
+ capitalization: "uppercase",
218
+ });
219
+ user.forgotPasswordRequestedAt = new Date();
220
+ await userModel.update(user);
221
+
222
+ await sendEmailForgotEmail(user);
223
+ }
224
+ ```
225
+
226
+ `sendAdminInviteEmail` takes a bare address and a token instead of a `User`, because an invitee has no user row until they sign up. The invite record itself, its token and its expiry belong to the [Core](/docs/interfaces/core/introduction) domain, whose invite helper dispatches this mail for you.
227
+
228
+ ## Password Policy
229
+
230
+ `@antelopejs/interface-dms/auth/password` exports the policy every DMS flow applies when a password is set: at least `PASSWORD_MIN_LENGTH` (8) characters, with at least one uppercase letter, one digit and one of `@$!%*?&`, and no character outside ASCII letters, digits and those special characters. `PASSWORD_PATTERN` holds the character rules without the length.
231
+
232
+ ```typescript
233
+ isPasswordCompliant(password: string): boolean
234
+ ```
235
+
236
+ It is a pure function, not an interface call, so it works without the DMS loaded. Call it from any flow you own that stores a password — a public signup, for instance — before hashing it, so your module accepts exactly the passwords the DMS accepts.
237
+
238
+ ## Announce a Registration
239
+
240
+ `announceRegistration` runs everything that must happen once an account has really joined a tenant: the welcome notification, the collaborator notification to the platform owners, and then the `USER_REGISTERED` hook.
241
+
242
+ ```typescript
243
+ announceRegistration(
244
+ userModel: UserModel,
245
+ user: User,
246
+ tenantId: string,
247
+ ): Promise<void>
248
+ ```
249
+
250
+ Call it from any registration flow you own — a module with its own signup, a SaaS transactional flow calling it once provisioning has committed. The DMS password signup and a provider login consuming an invitation both go through it, which is what keeps notifications and automations independent of which door the account came through.
251
+
252
+ The hook is observational: registration is already complete when it runs, so a failing subscriber is logged rather than allowed to fail the request and strand a user whose account exists but whose retry token is gone. The same is true of the notifications — they are dispatched in the background and a failure there is logged, not propagated.
253
+
254
+ ## Next Steps
255
+
256
+ [External Identities](/docs/interfaces/auth/external-identities) covers binding a user to the accounts they own on a login provider.
@@ -0,0 +1,122 @@
1
+ ---
2
+ title: "External Identities"
3
+ description: "An external identity binds an account a user owns on a login provider — GitHub, Google, whichever the deployment configures — to a `User` row, so that signing in through that provider resolves to the same account…"
4
+ ---
5
+
6
+ # External Identities
7
+
8
+ ## Overview
9
+
10
+ An external identity binds an account a user owns on a login provider — GitHub, Google, whichever the deployment configures — to a `User` row, so that signing in through that provider resolves to the same account every time. The binding is what the provider login flow reads; the provider account id, not the email, is what identifies it.
11
+
12
+ The two functions live on the root subpath, the table and its model on `/db`.
13
+
14
+ ```typescript
15
+ import {
16
+ getExternalIdentities,
17
+ linkExternalIdentity,
18
+ } from "@antelopejs/interface-dms/auth";
19
+ import {
20
+ USER_EXTERNAL_IDENTITIES_TABLE_NAME,
21
+ UserExternalIdentity,
22
+ UserExternalIdentityModel,
23
+ buildExternalIdentityId,
24
+ } from "@antelopejs/interface-dms/auth/db";
25
+ ```
26
+
27
+ This surface is scoped to authentication. A provider connection that powers a product feature — repository access, deployments — owns its own storage and must never be derived from these rows: they say who the user is, not what the deployment may do on their behalf.
28
+
29
+ ## Bind a Provider Account
30
+
31
+ ```typescript
32
+ linkExternalIdentity(
33
+ userId: string,
34
+ identity: ExternalIdentityInput,
35
+ ): Promise<ExternalIdentityLinkResult>
36
+ ```
37
+
38
+ ```typescript
39
+ interface ExternalIdentityInput {
40
+ provider: string;
41
+ providerAccountId: string;
42
+ email: string;
43
+ }
44
+
45
+ interface ExternalIdentityLinkResult {
46
+ wasCreated: boolean;
47
+ }
48
+ ```
49
+
50
+ | Field | Description |
51
+ | ----- | ----------- |
52
+ | `provider` | Provider identifier, as declared in the instance's OAuth configuration. |
53
+ | `providerAccountId` | Immutable account identifier on the provider side. Never the email: users change their provider email without changing account. |
54
+ | `email` | Email the provider reported, stored lowercased for support and audit only. |
55
+
56
+ `wasCreated` distinguishes a first binding from a refresh, which is what a login flow needs to decide whether the user just connected a new provider or simply signed in again.
57
+
58
+ ```typescript
59
+ import { linkExternalIdentity } from "@antelopejs/interface-dms/auth";
60
+
61
+ const { wasCreated } = await linkExternalIdentity(user._id, {
62
+ provider: "github",
63
+ providerAccountId: profile.id,
64
+ email: profile.email,
65
+ });
66
+ ```
67
+
68
+ The call is idempotent for the owning user: re-linking the same provider account only refreshes its `email` and `lastLoginAt`. Binding an account already owned by another user throws `409` with the message `error.oauth.identity_already_linked` — a provider account is a single-user credential.
69
+
70
+ Uniqueness is enforced by the row's deterministic primary key rather than by a secondary index, so two concurrent bindings of the same provider account collide instead of duplicating; the loser re-reads the row and either refreshes it or gets the `409`.
71
+
72
+ ## List a User's Identities
73
+
74
+ ```typescript
75
+ getExternalIdentities(userId: string): Promise<UserExternalIdentity[]>
76
+ ```
77
+
78
+ Returns an empty array for a user who only signs in with a password, which is how an account-settings screen decides whether to offer disconnecting a provider.
79
+
80
+ ## The `UserExternalIdentity` Table
81
+
82
+ Table `user_external_identities` in the DMS core schema, so its rows are shared across tenants like `users`. `USER_EXTERNAL_IDENTITIES_TABLE_NAME` holds the name.
83
+
84
+ | Field | Type | Description |
85
+ | ----- | ---- | ----------- |
86
+ | `_id` | `string` | `<provider>:<providerAccountId>`. Being the primary key is what makes the binding unique. |
87
+ | `userId` | `string` | Owning user. Indexed, related to `User`. |
88
+ | `provider` | `string` | Provider identifier. |
89
+ | `providerAccountId` | `string` | Account identifier on the provider side. |
90
+ | `email` | `string` | Email reported at the last successful login. Account resolution never reads it back. |
91
+ | `createdAt` | `Date` | Set on insert. Indexed. |
92
+ | `lastLoginAt` | `Date` | Refreshed on every successful login through this identity. |
93
+
94
+ `buildExternalIdentityId(provider, providerAccountId)` composes the id. Use it rather than formatting the string yourself, so a lookup by id keeps matching what `linkExternalIdentity` writes.
95
+
96
+ ## `UserExternalIdentityModel`
97
+
98
+ `UserExternalIdentityModel` extends `BasicDataModel(UserExternalIdentity, USER_EXTERNAL_IDENTITIES_TABLE_NAME)` and adds three methods:
99
+
100
+ | Method | Signature | Description |
101
+ | ------ | --------- | ----------- |
102
+ | `getByProviderAccount` | `(provider: string, providerAccountId: string) => Promise<UserExternalIdentity \| undefined>` | The identity a provider account is bound to, or `undefined` when the account is unknown. |
103
+ | `getByUserId` | `(userId: string) => Promise<UserExternalIdentity[]>` | Every identity bound to a user, read through the `userId` index. |
104
+ | `deleteByUserId` | `(userId: string) => Promise<void>` | Drop every identity of a user. |
105
+
106
+ `getByProviderAccount` is the login-side lookup: resolve the provider profile to an identity, and its `userId` to the account to sign in.
107
+
108
+ ```typescript
109
+ import { GetModel } from "@antelopejs/interface-database-decorators";
110
+ import {
111
+ UserExternalIdentityModel,
112
+ UserModel,
113
+ } from "@antelopejs/interface-dms/auth/db";
114
+
115
+ const identities = GetModel(UserExternalIdentityModel);
116
+ const identity = await identities.getByProviderAccount("github", profile.id);
117
+ const user = identity
118
+ ? await GetModel(UserModel).get(identity.userId)
119
+ : undefined;
120
+ ```
121
+
122
+ An unknown provider account means no account exists for it yet — hand the flow to signup or to invite redemption rather than creating one implicitly. `deleteByUserId` belongs to account deletion: leaving identities behind would let a recycled provider account resolve to a user row that is gone.
@@ -0,0 +1,2 @@
1
+ title: Base
2
+ icon: i-lucide-blocks
@@ -0,0 +1,202 @@
1
+ ---
2
+ title: "Introduction"
3
+ description: "The base domain is the component vocabulary of the DMS: forms, tables, charts and cards, layout containers, data types and shared prop types."
4
+ ---
5
+
6
+ # Introduction
7
+
8
+ ## Overview
9
+
10
+ The `base` domain of `@antelopejs/interface-dms` is the component vocabulary of the DMS: forms, tables, charts and cards, layout containers, trees, the data types behind every field, and the shared prop types they all draw from.
11
+
12
+ Every component here is a builder. `Form({...})`, `TableView(controller, {...})` and `ChartLine({...})` do not render anything — they return a `ComponentBuilder` from `@antelopejs/interface-dms/component`, which is a serializable description of a component: its name, its options, and its children. You assign that description to a static field of a page class, and the frontend reads the page's tree and draws it. The options you set are therefore data, and the components that need data are given a `fetchUrl` and a `submitUrl` pointing back at routes you declare on the same page or on a data controller.
13
+
14
+ Two consequences shape the whole domain. First, a component's contract is its props interface: what you can express is exactly what the props allow, which is why this reference documents them field by field. Second, anything the frontend must receive has to survive serialization, so a builder that accepts a nested `Component` — a modal target, a kanban card, a chart inside a card — stores its serialized form instead.
15
+
16
+ The DMS documentation carries task-shaped chapters on the same components, organised around what you are building. This folder is the contract behind them: the exact symbol names, option fields and return types.
17
+
18
+ ### Subpath Map
19
+
20
+ `@antelopejs/interface-dms/base` is the barrel over the modules below, and re-exports most of them. The ones marked as not in the barrel are imported from their own subpath. Every row is a subpath of `@antelopejs/interface-dms/base`.
21
+
22
+ | Subpath | Holds | In the root barrel | Documented in |
23
+ | --- | --- | --- | --- |
24
+ | `types` | Base props, colours, tones (`TONES`, `Tone`, `ICON_TONES`, `IconTone`), sizes, buttons, action targets, row-action rules, guards, watch actions, enums | yes | [Shared Types](/docs/interfaces/base/types) |
25
+ | `form` | `Form`, the field and group shapes, `FormComponents`, `FormEvents`, the schema helpers | yes | [Forms](/docs/interfaces/base/forms) |
26
+ | `table-view` | `TableView`, `@Column`, `TableViewRoutes`, guards, row actions | yes | [Tables](/docs/interfaces/base/tables) |
27
+ | `chart` | Chart builders, series and point shapes, chart props | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
28
+ | `chart-card` | `ChartCard` | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
29
+ | `kpi-card` | `KpiCard` | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
30
+ | `top-list-card` | `TopListCard` | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
31
+ | `period-selector` | `PeriodSelector` | yes | [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) |
32
+ | `live-toggle` | `LiveToggle` | yes | [Display Blocks](/docs/interfaces/base/display-blocks#live-toggle) |
33
+ | `grid` | `Grid`, `GridRow` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
34
+ | `stack` | `HStack`, `VStack`, `Spacer` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
35
+ | `tab` | `Tab` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
36
+ | `guard` | `Guard` | yes | [Layout and Containers](/docs/interfaces/base/layout#guard) |
37
+ | `layouts` | `DefaultLayout`, `FormPageLayout`, `SettingsLayout`, `EmptyLayout` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
38
+ | `placeholder` | `Placeholder` | yes | [Layout and Containers](/docs/interfaces/base/layout) |
39
+ | `display` | `BlockLinkAction`, `BlockItemsSource`, `BlockEmptyText` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
40
+ | `stat-group` | `StatGroup` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
41
+ | `key-value-list` | `KeyValueList` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
42
+ | `nav-card-grid` | `NavCardGrid` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
43
+ | `empty-state` | `EmptyState` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
44
+ | `banner` | `Banner` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
45
+ | `card` | `Card` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
46
+ | `code-block` | `CodeBlock` | yes | [Display Blocks](/docs/interfaces/base/display-blocks) |
47
+ | `custom` | `CustomComponent` | no | [Layout and Containers](/docs/interfaces/base/layout) |
48
+ | `data-types` | `DataType`, the registration decorators, `DefaultDataTypes`, `DefaultDataCompareTypes`, `StatusType` | no | [Data Types](/docs/interfaces/base/data-types) |
49
+ | `searchable` | `Searchable`, `SearchableMeta` | yes | [Helpers](/docs/interfaces/base/helpers) |
50
+ | `export-jobs` | `runExportJob` and the export-job lifecycle | yes | [Helpers](/docs/interfaces/base/helpers) |
51
+ | `tenant-export-archive` | `startTenantExportJob`, `buildTenantExportArchive` | yes | [Helpers](/docs/interfaces/base/helpers) |
52
+ | `helpers/file-refs` | File and image reference tracking | no | [Helpers](/docs/interfaces/base/helpers) |
53
+ | `helpers/archive-mode-helpers` | `applyArchiveModeDefaultRules` | no | [Helpers](/docs/interfaces/base/helpers) |
54
+
55
+ Within `types`, four modules are outside that subpath's barrel and are imported from `types/action-target`, `types/row-action`, `types/row-action-operators` and `types/guards`; the guard types and `RowActionRule` are also re-exported by `table-view`.
56
+
57
+ ## Key Features
58
+
59
+ - **Builders that produce data** — Every component function returns a `ComponentBuilder`, so a page's UI is a plain value you can factor, reuse and compose with `.child()`.
60
+ - **Forms from typed fields** — A field names a `DataType`, and the type supplies both the input component and the Zod schema; `Form` compiles the field list into one schema and ships its JSON Schema form with the component.
61
+ - **Tables from data controllers** — `TableView` reads a controller's data-api metadata and its `@Column` declarations to build the columns, the `new` / `edit` / `view` forms, and the permission-carrying actions in one call.
62
+ - **Server-enforced row rules and guards** — Row-action rules are a small algebra evaluated on both sides, and guards run inside the mutating routes, so what the row menu offers is what the server accepts.
63
+ - **A chart and widget set with a shared period scope** — Twelve chart types plus KPI, chart and top-list cards; one `PeriodSelector` id ties them to the same range and comparison.
64
+ - **Layout without a frontend build** — Grids, stacks, tabs and page layouts arrange components, and `CustomComponent` names a Vue component when the vocabulary runs out.
65
+ - **An extensible data-type registry** — `@RegisterDataType` and `@RegisterDataCompareMode` add your own types and filter operators to the catalogue the forms, columns and filters draw from.
66
+ - **Shared prop vocabulary** — Colours, sizes, HTTP methods, action targets, readonly behaviour and watch actions are declared once in `types` and reused by every component.
67
+
68
+ ## Dependencies
69
+
70
+ This interface relies on the following AntelopeJS packages:
71
+
72
+ - [@antelopejs/interface-api](https://github.com/AntelopeJS/interface-api) - Request context, controller metadata and route parameters.
73
+ - [@antelopejs/interface-api-util](https://github.com/AntelopeJS/interface-api-util) - `assert` for failing a route with an HTTP status.
74
+ - [@antelopejs/interface-core](https://github.com/AntelopeJS/interface-core) - Metadata storage, decorator factories, interface functions and logging.
75
+ - [@antelopejs/interface-data-api](https://github.com/AntelopeJS/interface-data-api) - Data controllers, default routes, list parameters and field metadata.
76
+ - [@antelopejs/interface-database](https://github.com/AntelopeJS/interface-database) - The value proxies compare modes build their filters from.
77
+ - [@antelopejs/interface-database-decorators](https://github.com/AntelopeJS/interface-database-decorators) - Models and the localization modifier.
78
+ - [@antelopejs/interface-file-storage](https://github.com/AntelopeJS/interface-file-storage) - Upload constraints, staged-key promotion and file deletion behind the file and image types.
79
+ - [zod](https://www.npmjs.com/package/zod) and [zod-to-json-schema](https://www.npmjs.com/package/zod-to-json-schema) - Field validation schemas and their JSON Schema form.
80
+
81
+ It also builds on other domains of the same package: `/component` for `Component` and `ComponentBuilder`, `/page` for the page a component is registered on, `/permissions` for the action permissions a table view declares, and `/auth` for the authenticated user its routes receive.
82
+
83
+ ## Quick Start
84
+
85
+ A page declaring one component. The `Form` builder describes the fields; the two routes on the same class serve and accept the values. Define the backing model first; `SettingsModel` is application code, not a DMS export.
86
+
87
+ ```typescript [db.ts]
88
+ import {
89
+ BasicDataModel,
90
+ Field,
91
+ RegisterTable,
92
+ Table,
93
+ } from "@antelopejs/interface-database-decorators";
94
+
95
+ export const SETTINGS_SCHEMA = "example_settings";
96
+ export const SETTINGS_ROW_ID = "module";
97
+
98
+ @RegisterTable("settings", SETTINGS_SCHEMA)
99
+ export class Settings extends Table {
100
+ @Field("string") declare _id: string;
101
+ @Field("boolean") declare enabled: boolean;
102
+ @Field("number") declare retentionDays: number;
103
+ }
104
+
105
+ export class SettingsModel extends BasicDataModel(Settings, "settings") {}
106
+ ```
107
+
108
+ The page edits one module-wide record, so restrict it to platform owners. It returns defaults before the first save and upserts only `SETTINGS_ROW_ID`, rather than updating every row in the table.
109
+
110
+ ```typescript [settings-page.ts]
111
+ import { Get, JSONBody, Post } from "@antelopejs/interface-api";
112
+ import { assert } from "@antelopejs/interface-api-util";
113
+ import { GetModel } from "@antelopejs/interface-database-decorators";
114
+ import {
115
+ PageController,
116
+ pagesCategory,
117
+ RegisterPage,
118
+ } from "@antelopejs/interface-dms/page";
119
+ import { DefaultDataTypes } from "@antelopejs/interface-dms/base/data-types";
120
+ import { Form, formSchema } from "@antelopejs/interface-dms/base/form";
121
+ import { DefaultLayout } from "@antelopejs/interface-dms/base/layouts";
122
+ import { HttpMethod } from "@antelopejs/interface-dms/base/types";
123
+ import { AuthOwnerOnly } from "@antelopejs/interface-dms/auth";
124
+ import { SettingsModel, SETTINGS_ROW_ID } from "./db";
125
+
126
+ @RegisterPage()
127
+ @AuthOwnerOnly()
128
+ export class SettingsPage extends PageController(
129
+ "settings",
130
+ {
131
+ urlSlug: "settings",
132
+ displayName: "Settings",
133
+ description: "Module configuration",
134
+ icon: "i-ph-gear",
135
+ category: pagesCategory,
136
+ },
137
+ DefaultLayout(),
138
+ ) {
139
+ static form = Form({
140
+ title: "Settings",
141
+ fetchUrl: "/settings/",
142
+ submitUrl: "/settings/",
143
+ submitUrlMethod: HttpMethod.post,
144
+ fields: [
145
+ {
146
+ id: "enabled",
147
+ label: "Enabled",
148
+ type: new DefaultDataTypes.BooleanType(),
149
+ },
150
+ {
151
+ id: "retentionDays",
152
+ label: "Retention (days)",
153
+ type: new DefaultDataTypes.NumberType({ min: 0 }),
154
+ required: true,
155
+ },
156
+ ],
157
+ });
158
+
159
+ settingsModel = GetModel(SettingsModel);
160
+
161
+ @Get("/")
162
+ async read() {
163
+ return await this.settingsModel.get(SETTINGS_ROW_ID)
164
+ ?? { enabled: false, retentionDays: 0 };
165
+ }
166
+
167
+ @Post("/")
168
+ async write(@JSONBody() body: unknown) {
169
+ const result = formSchema(SettingsPage.form).safeParse(body);
170
+ assert(result.success, 400, "Invalid settings");
171
+ await this.settingsModel.table.insert({
172
+ _id: SETTINGS_ROW_ID,
173
+ enabled: result.data.enabled ?? false,
174
+ retentionDays: result.data.retentionDays,
175
+ }, { conflict: "update" });
176
+ }
177
+ }
178
+ ```
179
+
180
+ Import the page and provision the schema from your module's entry file. Merge this into an existing `start()` if your module already has one:
181
+
182
+ ```typescript [index.ts]
183
+ import "./settings-page";
184
+ import { RegisterSchema } from "@antelopejs/interface-database-decorators";
185
+ import { SETTINGS_SCHEMA } from "./db";
186
+
187
+ export async function start(): Promise<void> {
188
+ await RegisterSchema(SETTINGS_SCHEMA);
189
+ }
190
+ ```
191
+
192
+ The page registers itself at import time, and its routes are mounted under the page's slug — which is why the form fetches from and submits to `/settings/`. `Form` serializes the field descriptions and JSON Schema for the frontend; the custom write route must still validate the request on the server, as the `formSchema` call does here. For per-tenant settings, use a tenant-scoped schema and model instead of this module-wide record.
193
+
194
+ ## Documentation Sections
195
+
196
+ - [Shared Types](/docs/interfaces/base/types) - Base props, colours, sizes, action targets, row-action rules, guards and the small enums.
197
+ - [Forms](/docs/interfaces/base/forms) - The `Form` builder, fields and groups, input components and the validation schema.
198
+ - [Tables](/docs/interfaces/base/tables) - Table views over a data controller: columns, routes, row actions, guards, displays and generated forms.
199
+ - [Charts and Widgets](/docs/interfaces/base/charts-and-widgets) - Chart types and props, chart cards, KPI cards, top lists and the period selector.
200
+ - [Layout and Containers](/docs/interfaces/base/layout) - Grids, stacks, tabs, the guard, page layouts, the placeholder and the custom-component escape hatch.
201
+ - [Data Types](/docs/interfaces/base/data-types) - The `DataType` contract, the registries, the default catalogue and a custom type.
202
+ - [Helpers](/docs/interfaces/base/helpers) - Searchable columns, export jobs, file references and the archive-mode defaults.