@protobase/schema 0.14.4 → 0.14.5

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.
@@ -1,13 +1,19 @@
1
- import { CheckedFilter } from "../model.js";
1
+ import { CheckedFilter, ResourceModel } from "../model.js";
2
2
  //#region src/filter/evaluate.d.ts
3
3
  /** How `evaluateFilter` evaluates. */
4
4
  export type EvaluateOptions = {
5
5
  /** Clock for `now()`; defaults to the current time. */
6
6
  now?: () => Date;
7
+ /**
8
+ * The resource the record is of: `search("text")` then looks in its search fields as the API's search does, instead of
9
+ * throwing `Not implemented`.
10
+ */
11
+ search?: ResourceModel;
7
12
  };
8
13
  /**
9
- * Evaluates a checked filter against a plain record keyed by field name, for rules such as
10
- * access filters on create. Throws `Not implemented` for `search` and `similar`.
14
+ * Evaluates a checked filter against a plain record keyed by field name, for rules such as access filters on create, or
15
+ * whether a new record fits a saved search. Throws `Not implemented` for `similar`, and for `search` without the
16
+ * `search` option.
11
17
  */
12
18
  export declare const evaluateFilter: (checked: CheckedFilter, record: unknown, options?: EvaluateOptions) => boolean;
13
19
  //#endregion
@@ -1,15 +1,16 @@
1
1
  import { functions } from "./profile.js";
2
2
  import { lift } from "./lift.js";
3
+ import { matchesSearch } from "../search-match.js";
3
4
  import { evaluateFilter } from "aip-parsers/filter";
4
5
  //#region src/filter/evaluate.ts
5
6
  const needsDatabase = (name) => () => {
6
7
  throw new Error(`Not implemented: ${name}() needs the database (the resource's search fields, trigram matching), so it cannot be evaluated against a record`);
7
8
  };
8
- const implementations = (now) => ({
9
+ const implementations = (now, record, model) => ({
9
10
  ...functions,
10
11
  search: {
11
12
  ...functions.search,
12
- evaluate: needsDatabase("search")
13
+ evaluate: model ? (text) => matchesSearch(model, record, text) : needsDatabase("search")
13
14
  },
14
15
  similar: {
15
16
  ...functions.similar,
@@ -25,9 +26,10 @@ const implementations = (now) => ({
25
26
  }
26
27
  });
27
28
  /**
28
- * Evaluates a checked filter against a plain record keyed by field name, for rules such as
29
- * access filters on create. Throws `Not implemented` for `search` and `similar`.
29
+ * Evaluates a checked filter against a plain record keyed by field name, for rules such as access filters on create, or
30
+ * whether a new record fits a saved search. Throws `Not implemented` for `similar`, and for `search` without the
31
+ * `search` option.
30
32
  */
31
- const evaluateFilter$1 = (checked, record, options = {}) => evaluateFilter(lift(checked, { expand: true }), record, { functions: implementations(options.now ?? (() => /* @__PURE__ */ new Date())) });
33
+ const evaluateFilter$1 = (checked, record, options = {}) => evaluateFilter(lift(checked, { expand: true }), record, { functions: implementations(options.now ?? (() => /* @__PURE__ */ new Date()), record, options.search) });
32
34
  //#endregion
33
35
  export { evaluateFilter$1 as evaluateFilter };
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 { ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, RelatedModel, ResourceModel, SaveFeedback, SearchMatch, SearchResultModel, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
2
+ import { ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, 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";
@@ -36,4 +36,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
36
36
  import { restrictModel } from "./access/restrict.js";
37
37
  import "./access/index.js";
38
38
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
39
- export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, 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 ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type Constraint, 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 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 Grant, type HeldCapability, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, 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 RecordKey, type RecordOf, type Ref, type Refs, 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 SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SignInPromptModel, 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, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, resolveAccess, resource, restrictModel, rule, userMenu, view, where };
39
+ export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, 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 ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type Constraint, 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 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 Grant, type HeldCapability, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, 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 RecordKey, type RecordOf, type Ref, type Refs, 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 SignInPromptModel, 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, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, resolveAccess, resource, restrictModel, rule, userMenu, view, where };
package/dist/internal.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
2
+ import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
2
3
  import { checkEventFilter } from "./events/event-models.js";
3
4
  import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
4
5
  import { addressPlaceholders, appHrefError, fillAddress, hrefPlaceholders, leavesApp } from "./address-template.js";
@@ -8,7 +9,6 @@ import { bottomBarFor } from "./bottom-bar.js";
8
9
  import { decodeKey, encodeKey } from "./keys.js";
9
10
  import { webhookResources } from "./webhooks/webhook-resources.js";
10
11
  import { pruneByShape, shapes } from "./ref-shape.js";
11
- import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
12
12
  import { inRange, pluginProblem } from "./plugin-version.js";
13
13
  import { redactSecrets } from "./redact-secrets.js";
14
14
  import { addressErrors } from "./address-errors.js";
package/dist/model.d.ts CHANGED
@@ -338,7 +338,8 @@ export type CardFavoriteModel = {
338
338
  };
339
339
  /**
340
340
  * What a visitor who is not signed in is told about a resource they could use once signed in (`.signIn(...)`): on its
341
- * list page, in place of the list, and in the dialog of a card's favorite button that saves to it. See
341
+ * list page, in place of the list, and in the dialog of a card's favorite button or a list's "Save search" button
342
+ * that saves to it. See
342
343
  * [Sign-in prompts](website/src/content/docs/reference/ui-config.mdx#sign-in-prompts).
343
344
  */
344
345
  export type SignInPromptModel = {
@@ -363,6 +364,44 @@ export type SignInViewModel = Pick<ViewModel, 'resource' | 'names' | 'nav'> & {
363
364
  /** What the visitor is told. */
364
365
  signIn: SignInPromptModel;
365
366
  };
367
+ /**
368
+ * The "Save search" button of a list page: it saves the list's filter as a record of `resource`, the signed-in user's
369
+ * own, whose view lists them (`.searchesOf(...)`). A visitor who is not signed in gets the resource's sign-in prompt
370
+ * instead. See [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
371
+ */
372
+ export type SaveSearchModel = {
373
+ /** The resource holding the saved searches, whose view has `searchesOf` for this one. */
374
+ resource: string;
375
+ /** The button's text, pressed while the filter is saved; "Save search" when omitted. */
376
+ label?: string;
377
+ };
378
+ /**
379
+ * What a saved search is (`.searchesOf(...)`): the list page shows each of the user's searches, with its filters, its
380
+ * `controls`, and the first records of `resource` it finds, newest first, and a link to all of them. See
381
+ * [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
382
+ */
383
+ export type SearchesOfModel = {
384
+ /** The resource searched, such as listings. */
385
+ resource: string;
386
+ /** The text field holding the search's filter, as the list's address has it. */
387
+ filter: string;
388
+ /** The text field a new search is named in, from its filters; the view's title shows it. */
389
+ name?: string;
390
+ /** Enum and boolean fields changed in place on each search, such as how often to hear of new records. */
391
+ controls?: string[];
392
+ /**
393
+ * With it, each search shows only the records new to the user: those whose `field` (of `resource`) is after the
394
+ * search's `seenAt` (a timestamp field of this resource), which opening all of its results sets to now.
395
+ */
396
+ unseen?: {
397
+ /** When the user last opened the search's results. */
398
+ seenAt: string;
399
+ /** The searched resource's timestamp that makes a record new, such as when it was published. */
400
+ field: string;
401
+ };
402
+ /** How many records each search shows; default 12. */
403
+ limit?: number;
404
+ };
366
405
  /** A view's list page (`.list(...)`). */
367
406
  export type ListModel = {
368
407
  /** The table's columns, in order. */
@@ -716,8 +755,12 @@ export type ViewModel = {
716
755
  actions: ActionModel[];
717
756
  /** The relation field whose records the list page shows instead of this resource's rows, in their own view's list. */
718
757
  listOf?: string;
719
- /** What a visitor who is not signed in is told in place of the list, and by a card's favorite button that saves here. */
758
+ /** What a visitor who is not signed in is told in place of the list, and by a favorite or "Save search" button that saves here. */
720
759
  signIn?: SignInPromptModel;
760
+ /** The list page's button that saves its filter as a search of the user's own. */
761
+ saveSearch?: SaveSearchModel;
762
+ /** The list page shows the user's saved searches of another resource, each with what it finds, instead of rows. */
763
+ searchesOf?: SearchesOfModel;
721
764
  };
722
765
  /** Where a part of a filter or order stands in its text, as character offsets. */
723
766
  export type Span = {
package/dist/view.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActionModel, ChartGranularity, ChartRange, FilterWidgetModel, LayoutItemModel, ListDisplayModel, NavModel, NavRecentModel, ResourceModel, SaveFeedback, SignInPromptModel, ViewModel } from "./model.js";
1
+ import { ActionModel, ChartGranularity, ChartRange, FilterWidgetModel, LayoutItemModel, ListDisplayModel, NavModel, NavRecentModel, ResourceModel, SaveFeedback, SaveSearchModel, SignInPromptModel, ViewModel } from "./model.js";
2
2
  import { FieldRefs, Ref } from "./refs.js";
3
3
  import { actions } from "./view-actions.js";
4
4
  import { displays } from "./view-displays.js";
@@ -106,13 +106,39 @@ export declare class ViewBuilder<R extends {
106
106
  listOf(pick: (r: FieldRefs<FieldsOf<R>>) => Ref): ViewBuilder<R>;
107
107
  /**
108
108
  * What a visitor who is not signed in is told about this resource, which they could use once signed in: on its list
109
- * page, in place of the list, and in the dialog of a card's favorite button that saves here. The page offers "Sign
110
- * in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
109
+ * page, in place of the list, and in the dialog of a card's favorite button or a list's "Save search" button that
110
+ * saves here. The page offers "Sign in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
111
111
  * bar and the sidebar of visitors. Throws when `signUp.href` is not `https:`, `http:` or `mailto:`. See
112
112
  * [Sign-in prompts](website/src/content/docs/reference/ui-config.mdx#sign-in-prompts).
113
113
  */
114
114
  signIn(prompt: SignInPromptModel): ViewBuilder<R>;
115
- /** With the resource's model, also verifies the title, search result and list display fields, `listOf` and the sidebar's `recent` group. */
115
+ /**
116
+ * A "Save search" button on the list page while its filter narrows it: it saves the filter as a record of `resource`,
117
+ * the signed-in user's own, whose view lists them with `.searchesOf(...)`, and takes it off again while pressed. A
118
+ * visitor is told what signing in gives, when that view has `.signIn(...)`. See
119
+ * [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
120
+ */
121
+ saveSearch(search: SaveSearchModel): ViewBuilder<R>;
122
+ /**
123
+ * This resource holds saved searches of `resource`, which a list page saves with `.saveSearch(...)`: its list page
124
+ * shows each of the user's searches with its filters, its `controls` to change in place, the newest records it finds
125
+ * (with `unseen`, only those new since the user last opened its results) and a link to all of them. Throws in
126
+ * `toModel` when `filter` or `name` is not a text field, `unseen.seenAt` not a timestamp or a control not an enum or
127
+ * boolean field.
128
+ * See [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
129
+ */
130
+ searchesOf(pick: (r: FieldRefs<FieldsOf<R>>) => {
131
+ resource: string;
132
+ filter: Ref;
133
+ name?: Ref;
134
+ controls?: Ref[];
135
+ unseen?: {
136
+ seenAt: Ref;
137
+ field: string;
138
+ };
139
+ limit?: number;
140
+ }): ViewBuilder<R>;
141
+ /** With the resource's model, also verifies the title, search result and list display fields, `listOf`, `searchesOf` and the sidebar's `recent` group. */
116
142
  toModel(resource?: ResourceModel): ViewModel;
117
143
  private refs;
118
144
  private next;
package/dist/view.js CHANGED
@@ -6,6 +6,16 @@ import { displayErrors, displayFields, displays } from "./view-displays.js";
6
6
  import { FieldView } from "./view-field.js";
7
7
  import { widgets } from "./view-widgets.js";
8
8
  //#region src/view.ts
9
+ const searchesOfErrors = (searches, resource) => {
10
+ const typed = (role, name, type) => name === void 0 || resource.fields[name]?.type === type ? [] : [`${role} "${name}" is not a ${type} field of the resource`];
11
+ return [
12
+ ...typed("filter", searches.filter, "text"),
13
+ ...typed("name", searches.name, "text"),
14
+ ...typed("unseen.seenAt", searches.unseen?.seenAt, "timestamp"),
15
+ ...(searches.controls ?? []).flatMap((name) => ["enum", "boolean"].includes(resource.fields[name]?.type ?? "") ? [] : [`control "${name}" is not an enum or boolean field of the resource`]),
16
+ ...searches.limit === void 0 || Number.isInteger(searches.limit) && searches.limit > 0 ? [] : ["limit must be a positive whole number"]
17
+ ];
18
+ };
9
19
  /**
10
20
  * A view of a resource, as `view(...)` starts it: how the admin shows it. Each method returns a new view, so one view
11
21
  * can build on another. See [UI config](website/src/content/docs/reference/ui-config.mdx).
@@ -122,8 +132,8 @@ var ViewBuilder = class ViewBuilder {
122
132
  }
123
133
  /**
124
134
  * What a visitor who is not signed in is told about this resource, which they could use once signed in: on its list
125
- * page, in place of the list, and in the dialog of a card's favorite button that saves here. The page offers "Sign
126
- * in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
135
+ * page, in place of the list, and in the dialog of a card's favorite button or a list's "Save search" button that
136
+ * saves here. The page offers "Sign in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
127
137
  * bar and the sidebar of visitors. Throws when `signUp.href` is not `https:`, `http:` or `mailto:`. See
128
138
  * [Sign-in prompts](website/src/content/docs/reference/ui-config.mdx#sign-in-prompts).
129
139
  */
@@ -131,7 +141,39 @@ var ViewBuilder = class ViewBuilder {
131
141
  if (prompt.signUp && !isMenuHref(prompt.signUp.href)) throw new Error(`View "${this.state.resource}": signIn signUp.href must start with https://, http:// or mailto:`);
132
142
  return this.next({ signIn: structuredClone(prompt) });
133
143
  }
134
- /** With the resource's model, also verifies the title, search result and list display fields, `listOf` and the sidebar's `recent` group. */
144
+ /**
145
+ * A "Save search" button on the list page while its filter narrows it: it saves the filter as a record of `resource`,
146
+ * the signed-in user's own, whose view lists them with `.searchesOf(...)`, and takes it off again while pressed. A
147
+ * visitor is told what signing in gives, when that view has `.signIn(...)`. See
148
+ * [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
149
+ */
150
+ saveSearch(search) {
151
+ return this.next({ saveSearch: { ...search } });
152
+ }
153
+ /**
154
+ * This resource holds saved searches of `resource`, which a list page saves with `.saveSearch(...)`: its list page
155
+ * shows each of the user's searches with its filters, its `controls` to change in place, the newest records it finds
156
+ * (with `unseen`, only those new since the user last opened its results) and a link to all of them. Throws in
157
+ * `toModel` when `filter` or `name` is not a text field, `unseen.seenAt` not a timestamp or a control not an enum or
158
+ * boolean field.
159
+ * See [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
160
+ */
161
+ searchesOf(pick) {
162
+ const { resource, filter, name, controls, unseen, limit } = pick(this.refs());
163
+ const searches = {
164
+ resource,
165
+ filter: filter.name,
166
+ ...name && { name: name.name },
167
+ ...controls && { controls: controls.map((control) => control.name) },
168
+ ...unseen && { unseen: {
169
+ seenAt: unseen.seenAt.name,
170
+ field: unseen.field
171
+ } },
172
+ ...limit !== void 0 && { limit }
173
+ };
174
+ return this.next({ searchesOf: searches });
175
+ }
176
+ /** With the resource's model, also verifies the title, search result and list display fields, `listOf`, `searchesOf` and the sidebar's `recent` group. */
135
177
  toModel(resource) {
136
178
  const { title, nav, searchResult, list, listOf } = this.state;
137
179
  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`);
@@ -145,6 +187,8 @@ var ViewBuilder = class ViewBuilder {
145
187
  if (flag !== void 0) throw new Error(`View "${this.state.resource}": display "${display.name}" shows "${flag}" as a flag, which is not a boolean field`);
146
188
  }
147
189
  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`);
190
+ const searchErrors = resource && this.state.searchesOf ? searchesOfErrors(this.state.searchesOf, resource) : [];
191
+ if (searchErrors.length > 0) throw new Error(`View "${this.state.resource}": searchesOf ${searchErrors.join("; ")}`);
148
192
  const recentErrors = resource && nav?.recent ? navRecentErrors(nav.recent, resource) : [];
149
193
  if (recentErrors.length > 0) throw new Error(`View "${this.state.resource}": nav.recent ${recentErrors.join("; ")}`);
150
194
  return structuredClone(this.state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.14.4",
3
+ "version": "0.14.5",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {