@protobase/schema 0.27.0 → 0.28.1

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.
@@ -4,7 +4,7 @@ import { ResolvedAccess } from "./resolve.js";
4
4
  /**
5
5
  * The resource as one user may see it: only readable fields remain, and a field is `readOnly`
6
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. The event catalogue, whose conditions may use any
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
8
8
  * field, is dropped. Key columns always stay,
9
9
  * because records are addressed by them.
10
10
  *
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * The resource as one user may see it: only readable fields remain, and a field is `readOnly`
4
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. The event catalogue, whose conditions may use any
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
6
6
  * field, is dropped. Key columns always stay,
7
7
  * because records are addressed by them.
8
8
  *
@@ -17,11 +17,12 @@ 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, events: _events, ...rest } = model;
20
+ const { search: _search, searchMatch: _searchMatch, tenant: _tenant, 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);
24
24
  const position = model.position && keep(model.position.field) && (model.position.within === void 0 || keep(model.position.within)) ? model.position : void 0;
25
+ const parents = model.parents?.filter((parent) => visible.has(parent.field));
25
26
  return {
26
27
  ...rest,
27
28
  fields: Object.fromEntries(Object.entries(model.fields).filter(([name]) => visible.has(name)).map(([name, field]) => [name, {
@@ -33,7 +34,8 @@ const restrictModel = (model, access) => {
33
34
  ...tenant && { tenant },
34
35
  ...softDelete && { softDelete },
35
36
  ...owner && { owner },
36
- ...position && { position: { ...position } }
37
+ ...position && { position: { ...position } },
38
+ ...parents?.length && { parents: parents.map((parent) => ({ ...parent })) }
37
39
  };
38
40
  };
39
41
  //#endregion
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Rollup, RollupCount, RollupFn, RollupSource, RollupValue } from "./computed/rollup.js";
2
2
  import { FileOptions, FileProcessor, FileProcessorInput, ImageFormat, ImageVariant, ImageVariantOptions, imageFormats } from "./files/file-options.js";
3
- import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CalendarView, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CommentsSectionModel, 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, GroupItemModel, GroupModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListDragModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NamedListModel, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, PositionAfterModel, RecordMode, RecordWhen, RelatedColumnModel, RelatedModel, RelatedPresentation, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, TabItemModel, TabModel, TimelineView, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
3
+ import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CalendarView, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CommentsSectionModel, 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, GroupItemModel, GroupModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListDragModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NamedListModel, NavModel, NavRecentModel, NowValue, OrderByItem, ParentAccess, ParentModel, PeriodChartModel, PositionAfterModel, RecordMode, RecordWhen, RelatedColumnModel, RelatedModel, RelatedPresentation, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, TabItemModel, TabModel, TimelineView, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
4
4
  import { AccessAction, AccessContext, AccessFn, AccessResult, AccessUser, FieldAccess, FieldRule } from "./access/types.js";
5
5
  import { AnyField, Constraint, Field, FieldMeta, FieldTypeDef } from "./field.js";
6
6
  import { RelationField } from "./relation-field.js";
@@ -43,4 +43,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
43
43
  import { restrictModel } from "./access/restrict.js";
44
44
  import "./access/index.js";
45
45
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
46
- 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 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 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 PeriodChartInput, type PeriodChartModel, type PositionAfterModel, 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, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, isRecordCondition, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
46
+ 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 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 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 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, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, isRecordCondition, 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
@@ -120,6 +120,8 @@ export type ResourceModel = {
120
120
  /** The records each record ranks after while they are pending, such as the tasks it waits for (`.position(..., { after })`). */
121
121
  after?: PositionAfterModel;
122
122
  };
123
+ /** The records its records belong to, and the access on them writing its records needs (`.parent()`). */
124
+ parents?: ParentModel[];
123
125
  /** The events its changes raise (`.events()`), the catalogue subscriptions pick from. */
124
126
  events?: EventModel[];
125
127
  /** Whether every change of its records is kept, with who made it (`.history()`). */
@@ -581,6 +583,20 @@ export type PositionAfterModel = {
581
583
  /** An AIP-160 filter on the records depended on, which they keep the order only while they match, such as `status != "done"`. */
582
584
  while?: string;
583
585
  };
586
+ /** The access on a parent record that writing a record under it needs: reading it, or updating it. */
587
+ export type ParentAccess = 'read' | 'update';
588
+ /**
589
+ * A record that a resource's records belong to through a relation field, such as an operation's VM: creating, changing,
590
+ * deleting or undeleting a record needs `access` on the record `field` points at, checked with the caller's own access
591
+ * rules on that resource. See
592
+ * [Records that belong to another](website/src/content/docs/reference/access.mdx#records-that-belong-to-another).
593
+ */
594
+ export type ParentModel = {
595
+ /** The relation field that points at the parent record. */
596
+ field: string;
597
+ /** The access on the parent record that writing needs. */
598
+ access: ParentAccess;
599
+ };
584
600
  /** A view's list page (`.list(...)`). */
585
601
  export type ListModel = {
586
602
  /** The table's columns, in order. */
@@ -1,4 +1,4 @@
1
- import { PositionAfterModel, SearchMatch } from "./model.js";
1
+ import { ParentModel, PositionAfterModel, SearchMatch } from "./model.js";
2
2
  import { FieldsInput } from "./refs.js";
3
3
  import { EventSpec } from "./events/event-helpers.js";
4
4
  //#region src/resource-model.d.ts
@@ -17,6 +17,7 @@ export type ResourceState = {
17
17
  within?: string;
18
18
  after?: PositionAfterModel;
19
19
  };
20
+ parents?: ParentModel[];
20
21
  events?: EventSpec[];
21
22
  history?: boolean;
22
23
  comments?: boolean;
@@ -70,7 +70,8 @@ const assembleModel = (state) => {
70
70
  ...state.history && { history: true },
71
71
  ...state.comments && { comments: true },
72
72
  ...state.live && { live: true },
73
- ...state.position && { position: { ...state.position } }
73
+ ...state.position && { position: { ...state.position } },
74
+ ...state.parents && { parents: state.parents.map((parent) => ({ ...parent })) }
74
75
  };
75
76
  };
76
77
  const sensitiveProblems = (model) => Object.values(model.fields).filter((field) => field.sensitive).flatMap((field) => [
@@ -130,6 +131,15 @@ const positionProblems = (model) => {
130
131
  ...model.position.after && model.primaryKey.length !== 1 ? [`position "${name}" ranks after other records, which needs a key of one field`] : []
131
132
  ];
132
133
  };
134
+ const parentProblems = (model) => (model.parents ?? []).flatMap(({ field: name, access }, index, parents) => {
135
+ const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
136
+ return [
137
+ ...!field ? ["is not a field"] : field.type !== "relation" ? [`is ${field.type}, not a relation`] : [],
138
+ ...field?.relation && field.relation.columns.length !== 1 ? ["is a relation of several columns (not implemented)"] : [],
139
+ ...access === "read" || access === "update" ? [] : [`needs "${String(access)}", not "read" or "update"`],
140
+ ...parents.findIndex((parent) => parent.field === name) !== index ? ["is given twice"] : []
141
+ ].map((problem) => `parent "${name}" ${problem}`);
142
+ });
133
143
  const aiProblems = (model) => Object.values(model.fields).filter((field) => field.ai).flatMap((field) => [
134
144
  ...field.type === "text" || field.type === "enum" ? [] : [`is ${field.type}, not text or an enum`],
135
145
  ...field.nullable ? [] : ["must be optional(), empty until the job fills it"],
@@ -150,6 +160,7 @@ const buildResourceModel = (state) => {
150
160
  ...sensitiveProblems(model),
151
161
  ...fileProblems(model),
152
162
  ...positionProblems(model),
163
+ ...parentProblems(model),
153
164
  ...aiProblems(model),
154
165
  ...computedProblems(model),
155
166
  ...events?.problems ?? []
@@ -1,4 +1,4 @@
1
- import { PositionAfterModel, ResourceModel, SearchMatch } from "./model.js";
1
+ import { ParentAccess, PositionAfterModel, ResourceModel, SearchMatch } from "./model.js";
2
2
  import { FieldAccess } from "./access/types.js";
3
3
  import { FieldRefs, FieldsInput, Ref } from "./refs.js";
4
4
  import { EventHelpers, EventMatch, EventSpec } from "./events/event-helpers.js";
@@ -86,6 +86,19 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
86
86
  within?: (r: FieldRefs<F>) => Ref;
87
87
  after?: PositionAfterModel;
88
88
  }): ResourceBuilder<F, K>;
89
+ /**
90
+ * A relation whose record each record belongs to, and the access on it writing them needs: creating a record, changing
91
+ * it (the parent it leaves and the one it moves to), deleting and undeleting it all need `access` on the parent,
92
+ * checked with the caller's own access rules on its resource and locked until the write commits. A null relation
93
+ * checks nothing; each call adds a check. See
94
+ * [Records that belong to another](website/src/content/docs/reference/access.mdx#records-that-belong-to-another).
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * .parent((r) => r.vm_id, 'update')
99
+ * ```
100
+ */
101
+ parent(pick: (r: FieldRefs<F>) => Ref, access: ParentAccess): ResourceBuilder<F, K>;
89
102
  /**
90
103
  * The events changes to its records raise, captured in the database for every write, from the API or elsewhere. See
91
104
  * [Declaring events](website/src/content/docs/reference/events.mdx#declaring-events).
package/dist/resource.js CHANGED
@@ -100,6 +100,24 @@ var ResourceBuilder = class ResourceBuilder {
100
100
  } });
101
101
  }
102
102
  /**
103
+ * A relation whose record each record belongs to, and the access on it writing them needs: creating a record, changing
104
+ * it (the parent it leaves and the one it moves to), deleting and undeleting it all need `access` on the parent,
105
+ * checked with the caller's own access rules on its resource and locked until the write commits. A null relation
106
+ * checks nothing; each call adds a check. See
107
+ * [Records that belong to another](website/src/content/docs/reference/access.mdx#records-that-belong-to-another).
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * .parent((r) => r.vm_id, 'update')
112
+ * ```
113
+ */
114
+ parent(pick, access) {
115
+ return this.next({ parents: [...this.state.parents ?? [], {
116
+ field: pick(this.refs()).name,
117
+ access
118
+ }] });
119
+ }
120
+ /**
103
121
  * The events changes to its records raise, captured in the database for every write, from the API or elsewhere. See
104
122
  * [Declaring events](website/src/content/docs/reference/events.mdx#declaring-events).
105
123
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {