@protobase/schema 0.21.6 → 0.21.7

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.
@@ -33,10 +33,10 @@ const limited = (role, capability, limit) => {
33
33
  const checkAnonymous = (held) => {
34
34
  for (const { capability, fields } of held) {
35
35
  const { name, action, scope } = parseCapability(capability);
36
- if (action !== "read") throw new Error(`The anonymous role may only read, but it grants "${capability}"`);
37
- if (name === "*") throw new Error(`The anonymous role must name each resource it reads, not "${capability}"`);
36
+ if (action !== "read" && action !== "create") throw new Error(`The anonymous role may only read, or create through a public form, but it grants "${capability}"`);
37
+ if (name === "*") throw new Error(`The anonymous role must name each resource it ${action}s, not "${capability}"`);
38
38
  if (scope !== void 0) throw new Error(`The anonymous role grants "${capability}", but an anonymous caller owns no rows`);
39
- if (!fields) throw new Error(`The anonymous role must list the fields it reads: { '${capability}': ['name', ...] }`);
39
+ if (!fields) throw new Error(`The anonymous role must list the fields it ${action}s: { '${capability}': ['name', ...] }`);
40
40
  }
41
41
  };
42
42
  /** A role's entries as the capabilities it holds; throws on anything that is not a capability or a valid limit. */
@@ -22,7 +22,8 @@ export type RoleLimit = HeldCapability & {
22
22
  *
23
23
  * An entry may limit capabilities to fields (an allowlist) and rows (an AIP-160 filter):
24
24
  * `{ 'products.read': ['sku', 'price'] }`, `{ 'products.read': { fields: ['name'], filter: 'published = true' } }`.
25
- * The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields.
25
+ * The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields, and
26
+ * create listed fields through a public form.
26
27
  * See [Access](website/src/content/docs/reference/access.mdx#capabilities).
27
28
  *
28
29
  * @example
@@ -11,7 +11,8 @@ const rolesOf = (user) => user.roles ?? [];
11
11
  *
12
12
  * An entry may limit capabilities to fields (an allowlist) and rows (an AIP-160 filter):
13
13
  * `{ 'products.read': ['sku', 'price'] }`, `{ 'products.read': { fields: ['name'], filter: 'published = true' } }`.
14
- * The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields.
14
+ * The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields, and
15
+ * create listed fields through a public form.
15
16
  * See [Access](website/src/content/docs/reference/access.mdx#capabilities).
16
17
  *
17
18
  * @example
@@ -0,0 +1,13 @@
1
+ //#region src/form-address.d.ts
2
+ /** The path of the public forms, below the app's base path: a form is at `/-/forms/<name>`. */
3
+ export declare const formRoute = "/-/forms";
4
+ /** Where a public form is: the form's name. */
5
+ export type FormAddress = {
6
+ /** The form's name, as `.form(...)` gives it. */
7
+ name: string;
8
+ };
9
+ /** The path below the base path of the public form `name`. */
10
+ export declare const formPath: (name: string) => string;
11
+ /** The public form the path below the base path shows; `undefined` for any other path. */
12
+ export declare const readFormAddress: (path: string) => FormAddress | undefined;
13
+ //#endregion
@@ -0,0 +1,15 @@
1
+ //#region src/form-address.ts
2
+ /** The path of the public forms, below the app's base path: a form is at `/-/forms/<name>`. */
3
+ const formRoute = "/-/forms";
4
+ /** What a form's name may hold, since it is its address: lowercase letters, digits, `-` and `_`. */
5
+ const formNamePattern = /^[a-z0-9][a-z0-9_-]*$/;
6
+ /** The path below the base path of the public form `name`. */
7
+ const formPath = (name) => `${formRoute}/${name}`;
8
+ /** The public form the path below the base path shows; `undefined` for any other path. */
9
+ const readFormAddress = (path) => {
10
+ if (!path.startsWith(`/-/forms/`)) return void 0;
11
+ const name = path.slice(9);
12
+ return formNamePattern.test(name) ? { name } : void 0;
13
+ };
14
+ //#endregion
15
+ export { formNamePattern, formPath, formRoute, readFormAddress };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FileOptions, FileProcessor, FileProcessorInput, ImageFormat, ImageVariant, ImageVariantOptions, imageFormats } from "./files/file-options.js";
2
- import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, GalleryModel, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
2
+ import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, FormModel, GalleryModel, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
3
3
  import { AccessAction, AccessContext, AccessFn, AccessResult, AccessUser, FieldAccess, FieldRule } from "./access/types.js";
4
4
  import { AnyField, Constraint, Field, FieldMeta, FieldTypeDef } from "./field.js";
5
5
  import { RelationField } from "./relation-field.js";
@@ -10,6 +10,7 @@ import { CreatedOptions, DeletedOptions, EventActor, EventContext, EventHelpers,
10
10
  import { Access, InferRecord, Issue, KeyOf, KeyRefs, RecordKey, RecordOf } from "./resource-types.js";
11
11
  import { ResourceBuilder, SearchRef, SearchRefs, Validator, resource } from "./resource.js";
12
12
  import { ChartInput, GroupChartInput, PeriodChartInput } from "./view-chart.js";
13
+ import { FormInput } from "./view-form.js";
13
14
  import { ListColumn } from "./view-columns.js";
14
15
  import { ComponentRef, ViewProps } from "./view-displays.js";
15
16
  import { FieldView } from "./view-field.js";
@@ -22,6 +23,7 @@ import { BottomBarNames, bottomBarErrors } from "./bottom-bar-errors.js";
22
23
  import { layout } from "./layout.js";
23
24
  import { KeyType, fitsKeyType, keyTypes } from "./keys.js";
24
25
  import { addressWords, appPath, readAppPath } from "./app-address.js";
26
+ import { FormAddress, formPath, formRoute, readFormAddress } from "./form-address.js";
25
27
  import { SignInAddress, SignInStep, readSignInAddress, signInParams, signInPath, signInRoute, signInSteps } from "./sign-in-address.js";
26
28
  import { FilterLimits, FilterParse, parseFilter } from "./filter/parse.js";
27
29
  import { CheckResult, checkFilter } from "./filter/check.js";
@@ -39,4 +41,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
39
41
  import { restrictModel } from "./access/restrict.js";
40
42
  import "./access/index.js";
41
43
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
42
- export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
44
+ export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type FormAddress, type FormInput, type FormModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { createWhere, where } from "./filter/where.js";
9
9
  import { anonymousRole } from "./access/role-entry.js";
10
10
  import { accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
11
11
  import { resource } from "./resource.js";
12
+ import { formPath, formRoute, readFormAddress } from "./form-address.js";
12
13
  import { fieldRef } from "./field-ref.js";
13
14
  import { view } from "./view.js";
14
15
  import { enumLabel } from "./enum-label.js";
@@ -23,4 +24,4 @@ import { rule } from "./access/rule.js";
23
24
  import { defineRoles } from "./access/roles.js";
24
25
  import { pickView } from "./access/pick-view.js";
25
26
  import { restrictModel } from "./access/restrict.js";
26
- export { accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
27
+ export { accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
package/dist/model.d.ts CHANGED
@@ -1000,6 +1000,25 @@ export type ViewModel = {
1000
1000
  export?: false | ExportModel;
1001
1001
  /** The page that creates the resource's records, in place of the create form. */
1002
1002
  create?: CreateModel;
1003
+ /** Public forms that create its records for people without an account, each at `/-/forms/<name>`. */
1004
+ forms?: FormModel[];
1005
+ };
1006
+ /**
1007
+ * A public form (`.form(...)`): anyone, signed in or not, opens it at `/-/forms/<name>` and creates a record with its
1008
+ * fields, through the `anonymous` role's `create`, behind a rate limit and a bot check. See
1009
+ * [Public forms](website/src/content/docs/reference/forms.mdx).
1010
+ */
1011
+ export type FormModel = {
1012
+ /** Its address in the app, `/-/forms/<name>`; unique across the app. */
1013
+ name: string;
1014
+ /** The page's heading; the view's singular name when omitted. */
1015
+ title?: string;
1016
+ /** The fields a visitor fills in, in order. */
1017
+ fields: string[];
1018
+ /** What the page shows once the record is created. */
1019
+ thanks: string;
1020
+ /** How many submissions one client may send per minute. */
1021
+ perMinute: number;
1003
1022
  };
1004
1023
  /** How the resource's records are created (`.create(...)`): a page of the app's own. */
1005
1024
  export type CreateModel = {
@@ -0,0 +1,16 @@
1
+ import { Ref } from "./refs.js";
2
+ //#region src/view-form.d.ts
3
+ /** What `.form(...)` takes. */
4
+ export type FormInput = {
5
+ /** Its address in the app, `/-/forms/<name>`: lowercase letters, digits, `-` and `_`, unique across the app. */
6
+ name: string;
7
+ /** The page's heading; the view's singular name when omitted. */
8
+ title?: string;
9
+ /** The fields a visitor fills in, in order: plain fields the `anonymous` role may create. */
10
+ fields: Ref[];
11
+ /** What the page shows once the record is created. */
12
+ thanks: string;
13
+ /** How many submissions one client may send per minute; default 5. */
14
+ perMinute?: number;
15
+ };
16
+ //#endregion
@@ -0,0 +1,22 @@
1
+ import { formNamePattern } from "./form-address.js";
2
+ const formModel = (input) => {
3
+ const { name, title, fields, thanks } = input;
4
+ if (!formNamePattern.test(name)) throw new Error(`A form's name goes in its address, so it takes lowercase letters, digits, - and _, not "${name}"`);
5
+ if (fields.length === 0) throw new Error(`Form "${name}" has no fields`);
6
+ const names = fields.map((field) => field.name);
7
+ const repeated = names.find((field, index) => names.indexOf(field) !== index);
8
+ if (repeated !== void 0) throw new Error(`Form "${name}" lists the field "${repeated}" twice`);
9
+ if (thanks.trim() === "") throw new Error(`Form "${name}" needs a thanks text, shown once a visitor sends it`);
10
+ if (title !== void 0 && title.trim() === "") throw new Error(`Form "${name}" has an empty title; leave it out for the view's name`);
11
+ const perMinute = input.perMinute ?? 5;
12
+ if (!Number.isInteger(perMinute) || perMinute < 1) throw new Error(`Form "${name}" takes perMinute as a positive whole number, not ${perMinute}`);
13
+ return {
14
+ name,
15
+ ...title !== void 0 && { title },
16
+ fields: names,
17
+ thanks,
18
+ perMinute
19
+ };
20
+ };
21
+ //#endregion
22
+ export { formModel };
package/dist/view.d.ts CHANGED
@@ -2,6 +2,7 @@ import { ActionModel, CreateModel, ExportModel, FilterWidgetModel, HelpWhen, Lay
2
2
  import { FieldRefs, Ref } from "./refs.js";
3
3
  import { actions } from "./view-actions.js";
4
4
  import { ChartInput } from "./view-chart.js";
5
+ import { FormInput } from "./view-form.js";
5
6
  import { ListColumn, columns } from "./view-columns.js";
6
7
  import { displays } from "./view-displays.js";
7
8
  import { FieldView } from "./view-field.js";
@@ -97,6 +98,14 @@ export declare class ViewBuilder<R extends {
97
98
  * 10), counted or with `sum` or `average` that of a numeric field.
98
99
  */
99
100
  chart(build: (r: FieldRefs<FieldsOf<R>>) => ChartInput): ViewBuilder<R>;
101
+ /**
102
+ * A public form that creates a record of this resource for anyone, signed in or not, at `/-/forms/<name>`: the
103
+ * `fields` a visitor fills in, the `thanks` shown once it is sent, a `title` (the view's singular name by default) and
104
+ * how many submissions one client may send `perMinute` (default 5). The `anonymous` role must grant `create` of those
105
+ * fields; the form is the only way it creates. Each call adds a form. Throws on a name that does not fit an address, no
106
+ * or repeated fields, or an empty `thanks`. See [Public forms](website/src/content/docs/reference/forms.mdx).
107
+ */
108
+ form(build: (r: FieldRefs<FieldsOf<R>>) => FormInput): ViewBuilder<R>;
100
109
  /**
101
110
  * How the record page opens to a user who may change the record: `edit`, the default, as its form; `read` as a read
102
111
  * view, its sections as tables of labels and values, with an Edit button to the form; `locked` as the form, locked
@@ -178,7 +187,7 @@ export declare class ViewBuilder<R extends {
178
187
  };
179
188
  limit?: number;
180
189
  }): ViewBuilder<R>;
181
- /** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf` and the sidebar's `recent` group. */
190
+ /** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf`, the sidebar's `recent` group and the forms' fields. */
182
191
  toModel(resource?: ResourceModel): ViewModel;
183
192
  private refs;
184
193
  private next;
package/dist/view.js CHANGED
@@ -3,6 +3,7 @@ import { isMenuHref } from "./menu-href.js";
3
3
  import { navRecentErrors } from "./nav-recent.js";
4
4
  import { actions } from "./view-actions.js";
5
5
  import { chartModel } from "./view-chart.js";
6
+ import { formModel } from "./view-form.js";
6
7
  import { columns, isListColumn } from "./view-columns.js";
7
8
  import { boardErrors, displayErrors, displayFields, displays } from "./view-displays.js";
8
9
  import { FieldView } from "./view-field.js";
@@ -124,6 +125,16 @@ var ViewBuilder = class ViewBuilder {
124
125
  return this.next({ chart: chartModel(build(this.refs())) });
125
126
  }
126
127
  /**
128
+ * A public form that creates a record of this resource for anyone, signed in or not, at `/-/forms/<name>`: the
129
+ * `fields` a visitor fills in, the `thanks` shown once it is sent, a `title` (the view's singular name by default) and
130
+ * how many submissions one client may send `perMinute` (default 5). The `anonymous` role must grant `create` of those
131
+ * fields; the form is the only way it creates. Each call adds a form. Throws on a name that does not fit an address, no
132
+ * or repeated fields, or an empty `thanks`. See [Public forms](website/src/content/docs/reference/forms.mdx).
133
+ */
134
+ form(build) {
135
+ return this.next({ forms: [...this.state.forms ?? [], formModel(build(this.refs()))] });
136
+ }
137
+ /**
127
138
  * How the record page opens to a user who may change the record: `edit`, the default, as its form; `read` as a read
128
139
  * view, its sections as tables of labels and values, with an Edit button to the form; `locked` as the form, locked
129
140
  * until Edit is pressed. A user who may not change it gets the read view in every mode. The form has its own address
@@ -230,7 +241,7 @@ var ViewBuilder = class ViewBuilder {
230
241
  };
231
242
  return this.next({ searchesOf: searches });
232
243
  }
233
- /** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf` and the sidebar's `recent` group. */
244
+ /** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf`, the sidebar's `recent` group and the forms' fields. */
234
245
  toModel(resource) {
235
246
  const { title, nav, searchResult, list, listOf } = this.state;
236
247
  if (resource && title !== void 0 && !Object.hasOwn(resource.fields, title)) throw new Error(`View "${this.state.resource}": title field "${title}" does not exist on the resource`);
@@ -255,6 +266,10 @@ var ViewBuilder = class ViewBuilder {
255
266
  if (resource && listOf !== void 0 && (!Object.hasOwn(resource.fields, listOf) || resource.fields[listOf].type !== "relation")) throw new Error(`View "${this.state.resource}": listOf names "${listOf}", which is not a relation field of the resource`);
256
267
  const searchErrors = resource && this.state.searchesOf ? searchesOfErrors(this.state.searchesOf, resource) : [];
257
268
  if (searchErrors.length > 0) throw new Error(`View "${this.state.resource}": searchesOf ${searchErrors.join("; ")}`);
269
+ for (const form of resource ? this.state.forms ?? [] : []) {
270
+ const unknown = form.fields.find((name) => !Object.hasOwn(resource.fields, name));
271
+ if (unknown !== void 0) throw new Error(`View "${this.state.resource}": form "${form.name}" names field "${unknown}", which the resource does not have`);
272
+ }
258
273
  const recentErrors = resource && nav?.recent ? navRecentErrors(nav.recent, resource) : [];
259
274
  if (recentErrors.length > 0) throw new Error(`View "${this.state.resource}": nav.recent ${recentErrors.join("; ")}`);
260
275
  return structuredClone(this.state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.21.6",
3
+ "version": "0.21.7",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {