@protobase/schema 0.46.6 → 0.46.8

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.
@@ -3,8 +3,8 @@ import { ResolvedAccess } from "./resolve.js";
3
3
  //#region src/access/restrict.d.ts
4
4
  /**
5
5
  * The resource as one user may see it: only readable fields remain, and a field is `readOnly`
6
- * unless it is writable on create or update. `search` (and `searchMatch`) shrinks to readable fields, and `tenant`,
7
- * `softDelete` and `owner` are dropped when their field is hidden, `position` when its field or its `within` is, and each of `parents` whose field is. The event catalogue, whose conditions may use any
6
+ * unless it is writable on create or update. `search` (and `searchMatch`) shrinks to readable fields, and `tenant` (with
7
+ * `tenantAcross`), `softDelete` and `owner` are dropped when their field is hidden, `position` when its field or its `within` is, and each of `parents` whose field is. The event catalogue, whose conditions may use any
8
8
  * field, is dropped. Key columns always stay,
9
9
  * because records are addressed by them.
10
10
  *
@@ -1,8 +1,8 @@
1
1
  //#region src/access/restrict.ts
2
2
  /**
3
3
  * The resource as one user may see it: only readable fields remain, and a field is `readOnly`
4
- * unless it is writable on create or update. `search` (and `searchMatch`) shrinks to readable fields, and `tenant`,
5
- * `softDelete` and `owner` are dropped when their field is hidden, `position` when its field or its `within` is, and each of `parents` whose field is. The event catalogue, whose conditions may use any
4
+ * unless it is writable on create or update. `search` (and `searchMatch`) shrinks to readable fields, and `tenant` (with
5
+ * `tenantAcross`), `softDelete` and `owner` are dropped when their field is hidden, `position` when its field or its `within` is, and each of `parents` whose field is. The event catalogue, whose conditions may use any
6
6
  * field, is dropped. Key columns always stay,
7
7
  * because records are addressed by them.
8
8
  *
@@ -17,7 +17,7 @@ const restrictModel = (model, access) => {
17
17
  const keep = (name) => name !== void 0 && visible.has(name) ? name : void 0;
18
18
  const search = model.search?.filter((name) => visible.has(name));
19
19
  const searchMatch = model.searchMatch && Object.entries(model.searchMatch).filter(([name]) => visible.has(name));
20
- const { search: _search, searchMatch: _searchMatch, tenant: _tenant, softDelete: _softDelete, owner: _owner, position: _position, parents: _parents, events: _events, ...rest } = model;
20
+ const { search: _search, searchMatch: _searchMatch, tenant: _tenant, tenantAcross: _tenantAcross, softDelete: _softDelete, owner: _owner, position: _position, parents: _parents, events: _events, ...rest } = model;
21
21
  const tenant = keep(model.tenant);
22
22
  const softDelete = keep(model.softDelete);
23
23
  const owner = keep(model.owner);
@@ -32,6 +32,7 @@ const restrictModel = (model, access) => {
32
32
  ...search?.length && { search },
33
33
  ...searchMatch?.length && { searchMatch: Object.fromEntries(searchMatch) },
34
34
  ...tenant && { tenant },
35
+ ...tenant && model.tenantAcross && { tenantAcross: model.tenantAcross },
35
36
  ...softDelete && { softDelete },
36
37
  ...owner && { owner },
37
38
  ...position && { position: { ...position } },
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ import { f } from "./fields.js";
10
10
  import { FieldRefs, FieldsInput, Ref, Refs } from "./refs.js";
11
11
  import { CreatedOptions, DeletedOptions, EventActor, EventContext, EventHelpers, EventMatch, EventSpec, UndeletedOptions, UpdatedOptions } from "./events/event-helpers.js";
12
12
  import { Access, InferRecord, Issue, KeyOf, KeyRefs, RecordKey, RecordOf } from "./resource-types.js";
13
- import { ResourceBuilder, SearchRef, SearchRefs, Validator, resource } from "./resource.js";
13
+ import { ResourceBuilder, SearchRef, SearchRefs, TenantOptions, Validator, resource } from "./resource.js";
14
14
  import { isRecordCondition } from "./record-when.js";
15
15
  import { ChartInput, GroupChartInput, PeriodChartInput } from "./view-chart.js";
16
16
  import { FormInput } from "./view-form.js";
@@ -45,4 +45,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
45
45
  import { restrictModel } from "./access/restrict.js";
46
46
  import "./access/index.js";
47
47
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
48
- 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 CalendarView, 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 CommentsSectionModel, type CompareOp, type ComponentRef, type ConnectAddress, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type Encrypted, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExactCapability, 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 GroupItemModel, type GroupModel, 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 ListDragModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NamedListModel, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type ParentAccess, type ParentModel, type PeriodChartInput, type PeriodChartModel, type PositionAfterModel, type RecordChartModel, type RecordChartRange, type RecordKey, type RecordMode, type RecordOf, type RecordWhen, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelatedPresentation, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type Rollup, type RollupCount, type RollupFn, type RollupSource, type RollupValue, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type TabItemModel, type TabModel, type TimelineView, 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, connectParams, connectPath, connectRoute, createWhere, defineRoles, encrypted, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, isRecordCondition, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readConnectAddress, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
48
+ 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 CalendarView, 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 CommentsSectionModel, type CompareOp, type ComponentRef, type ConnectAddress, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type Encrypted, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExactCapability, 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 GroupItemModel, type GroupModel, 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 ListDragModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NamedListModel, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type ParentAccess, type ParentModel, type PeriodChartInput, type PeriodChartModel, type PositionAfterModel, type RecordChartModel, type RecordChartRange, type RecordKey, type RecordMode, type RecordOf, type RecordWhen, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelatedPresentation, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type Rollup, type RollupCount, type RollupFn, type RollupSource, type RollupValue, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type TabItemModel, type TabModel, type TenantOptions, type TimelineView, 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, connectParams, connectPath, connectRoute, createWhere, defineRoles, encrypted, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, isRecordCondition, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readConnectAddress, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
package/dist/model.d.ts CHANGED
@@ -112,6 +112,11 @@ export type ResourceModel = {
112
112
  softDelete?: string;
113
113
  /** The field that separates tenants: every query is limited to the caller's tenant (`.tenant()`). */
114
114
  tenant?: string;
115
+ /**
116
+ * `memberships`: lists also read the other tenants the caller is a member of, each as the caller's role there allows
117
+ * (`.tenant(pick, { across })`); a record's own reads and every write stay in the caller's tenant.
118
+ */
119
+ tenantAcross?: 'memberships';
115
120
  /** Field names that the bare-text `search` filter matches against. */
116
121
  search?: string[];
117
122
  /** How a search field matches when not by containing the word; see `SearchMatch`. */
@@ -1114,6 +1119,12 @@ export type NavModel = {
1114
1119
  order?: number;
1115
1120
  /** A few of its records listed under its entry. */
1116
1121
  recent?: NavRecentModel;
1122
+ /**
1123
+ * `current`: the entry opens the list on the caller's own tenant, and its recent records are that tenant's, for a
1124
+ * resource whose lists read across tenants (`.tenant(pick, { across })`). `/meta` writes it into the list's opening
1125
+ * filter and the recent records' filter as `tenantField = "<tenant>"`, which the person can clear or change.
1126
+ */
1127
+ tenant?: 'current';
1117
1128
  };
1118
1129
  /** An entry in the menu under the signed-in user, at the bottom of the sidebar. */
1119
1130
  export type UserMenuItemModel = {
@@ -9,6 +9,7 @@ export type ResourceState = {
9
9
  primaryKey?: string[];
10
10
  softDelete?: string;
11
11
  tenant?: string;
12
+ tenantAcross?: 'memberships';
12
13
  search?: string[];
13
14
  searchMatch?: Record<string, SearchMatch>;
14
15
  owner?: string;
@@ -65,6 +65,7 @@ const assembleModel = (state) => {
65
65
  primaryKey: [...primaryKey],
66
66
  ...state.softDelete && { softDelete: state.softDelete },
67
67
  ...state.tenant && { tenant: state.tenant },
68
+ ...state.tenant && state.tenantAcross && { tenantAcross: state.tenantAcross },
68
69
  ...state.search && { search: [...state.search] },
69
70
  ...state.searchMatch && Object.keys(state.searchMatch).length > 0 && { searchMatch: { ...state.searchMatch } },
70
71
  ...state.owner && { owner: state.owner },
@@ -17,6 +17,14 @@ export type SearchRefs<F> = { readonly [K in keyof FieldRefs<F>]: FieldRefs<F>[K
17
17
  /** Matches only the end of the field's digits, ignoring spaces, `+` and dashes, for phone numbers. */
18
18
  digitsEnd(): SearchRef;
19
19
  }; };
20
+ /** How `.tenant()` separates tenants. */
21
+ export type TenantOptions = {
22
+ /**
23
+ * `memberships`: lists also read every other tenant the caller is a member of, each with the caller's role there,
24
+ * so the tenant field works as a filter across them. A record's own reads and every write stay in the caller's tenant.
25
+ */
26
+ across?: 'memberships';
27
+ };
20
28
  /** A rule over a whole record for `.validate()`: the issues it finds, or `undefined` when there are none. */
21
29
  export type Validator<F> = (record: RecordOf<F>) => Issue<F>[] | undefined;
22
30
  /**
@@ -58,9 +66,10 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
58
66
  softDelete(pick: (r: FieldRefs<F>) => Ref): ResourceBuilder<F, K>;
59
67
  /**
60
68
  * The field that separates tenants: every query is limited to the caller's tenant, creates set it, and a request body
61
- * may not.
69
+ * may not. With `across: 'memberships'`, its lists also read the other tenants the caller is a member of, each as the
70
+ * caller's role there allows; see [Lists across organizations](website/src/content/docs/reference/access.mdx#lists-across-organizations).
62
71
  */
63
- tenant(pick: (r: FieldRefs<F>) => Ref): ResourceBuilder<F, K>;
72
+ tenant(pick: (r: FieldRefs<F>) => Ref, options?: TenantOptions): ResourceBuilder<F, K>;
64
73
  /**
65
74
  * Fields matched by `search("...")` and bare words in filters. Authoritative over the UI's list search defaults.
66
75
  * `r.phone.digitsEnd()` matches only the end of the field's digits, for phone numbers.
package/dist/resource.js CHANGED
@@ -50,10 +50,14 @@ var ResourceBuilder = class ResourceBuilder {
50
50
  }
51
51
  /**
52
52
  * The field that separates tenants: every query is limited to the caller's tenant, creates set it, and a request body
53
- * may not.
53
+ * may not. With `across: 'memberships'`, its lists also read the other tenants the caller is a member of, each as the
54
+ * caller's role there allows; see [Lists across organizations](website/src/content/docs/reference/access.mdx#lists-across-organizations).
54
55
  */
55
- tenant(pick) {
56
- return this.next({ tenant: pick(this.refs()).name });
56
+ tenant(pick, options = {}) {
57
+ return this.next({
58
+ tenant: pick(this.refs()).name,
59
+ ...options.across && { tenantAcross: options.across }
60
+ });
57
61
  }
58
62
  /**
59
63
  * Fields matched by `search("...")` and bare words in filters. Authoritative over the UI's list search defaults.
package/dist/view.d.ts CHANGED
@@ -61,7 +61,11 @@ export declare class ViewBuilder<R extends {
61
61
  title?: Ref | Ref[];
62
62
  subtitle?: Ref;
63
63
  }): ViewBuilder<R>;
64
- /** Where the resource sits in the sidebar; `recent` lists a few of its records under the entry (see [Sidebar](website/src/content/docs/reference/sidebar.md#recent-records)). */
64
+ /**
65
+ * Where the resource sits in the sidebar; `recent` lists a few of its records under the entry (see
66
+ * [Sidebar](website/src/content/docs/reference/sidebar.md#recent-records)), and `tenant: 'current'` opens it on the
67
+ * caller's own tenant (see [The current organization](website/src/content/docs/reference/sidebar.md#the-current-organization)).
68
+ */
65
69
  nav(nav: NavInput<R>): ViewBuilder<R>;
66
70
  /** A line under the list's title. */
67
71
  help(help: string): ViewBuilder<R>;
@@ -197,7 +201,7 @@ export declare class ViewBuilder<R extends {
197
201
  };
198
202
  limit?: number;
199
203
  }): ViewBuilder<R>;
200
- /** 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. */
204
+ /** 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 `tenant`, and the forms' fields. */
201
205
  toModel(resource?: ResourceModel): ViewModel;
202
206
  private refs;
203
207
  private next;
package/dist/view.js CHANGED
@@ -69,7 +69,11 @@ var ViewBuilder = class ViewBuilder {
69
69
  ...subtitle && { subtitle: subtitle.name }
70
70
  } });
71
71
  }
72
- /** Where the resource sits in the sidebar; `recent` lists a few of its records under the entry (see [Sidebar](website/src/content/docs/reference/sidebar.md#recent-records)). */
72
+ /**
73
+ * Where the resource sits in the sidebar; `recent` lists a few of its records under the entry (see
74
+ * [Sidebar](website/src/content/docs/reference/sidebar.md#recent-records)), and `tenant: 'current'` opens it on the
75
+ * caller's own tenant (see [The current organization](website/src/content/docs/reference/sidebar.md#the-current-organization)).
76
+ */
73
77
  nav(nav) {
74
78
  return this.next({ nav });
75
79
  }
@@ -283,7 +287,7 @@ var ViewBuilder = class ViewBuilder {
283
287
  };
284
288
  return this.next({ searchesOf: searches });
285
289
  }
286
- /** 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. */
290
+ /** 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 `tenant`, and the forms' fields. */
287
291
  toModel(resource) {
288
292
  const { title, nav, searchResult, list, listOf } = this.state;
289
293
  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`);
@@ -331,6 +335,9 @@ var ViewBuilder = class ViewBuilder {
331
335
  const unknown = form.fields.find((name) => !Object.hasOwn(resource.fields, name));
332
336
  if (unknown !== void 0) throw new Error(`View "${this.state.resource}": form "${form.name}" names field "${unknown}", which the resource does not have`);
333
337
  }
338
+ if (nav?.tenant === "current" && !list) throw new Error(`View "${this.state.resource}": nav.tenant narrows the opening filter of the list, so the view needs .list(...)`);
339
+ const tenantField = resource?.tenant === void 0 ? void 0 : resource.fields[resource.tenant];
340
+ if (resource && nav?.tenant === "current" && !tenantField?.filterable) throw new Error(`View "${this.state.resource}": nav.tenant opens the list on the current tenant, so the resource needs .tenant() on a .filterable() field`);
334
341
  const recentErrors = resource && nav?.recent ? navRecentErrors(nav.recent, resource) : [];
335
342
  if (recentErrors.length > 0) throw new Error(`View "${this.state.resource}": nav.recent ${recentErrors.join("; ")}`);
336
343
  if (this.state.lists?.length && (listOf !== void 0 || this.state.searchesOf)) throw new Error(`View "${this.state.resource}": named lists cannot go with listOf or searchesOf, which replace the list page`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.46.6",
3
+ "version": "0.46.8",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {