@protobase/server 0.19.12 → 0.19.14

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,6 +3,7 @@ import { cardPhotosErrors } from "./view-card-photos.js";
3
3
  import { cardFavoriteErrors, favoriteOwnerErrors } from "./view-card-favorite.js";
4
4
  import { saveSearchErrors, searchesOfErrors } from "./view-saved-search.js";
5
5
  import { mapDisplayErrors, mapErrors } from "./view-map.js";
6
+ import { relatedColumnErrors } from "./view-related-column.js";
6
7
  import { addressErrors, addressPlaceholders, displayFields, fieldRefsIn, hrefField, hrefPlaceholders, linkActionError } from "@protobase/schema/internal";
7
8
  //#region src/check-views.ts
8
9
  const linkFields = (run) => run?.kind !== "link" ? [] : "to" in run ? addressPlaceholders(run.to) : hrefPlaceholders(run.href);
@@ -79,6 +80,11 @@ const sharedPhotosErrors = (view) => {
79
80
  const same = (a, b) => a.resource === b.resource && a.field === b.field && a.image === b.image && a.sort === b.sort;
80
81
  return shown.filter(({ photos }) => !same(photos, shown[0].photos)).map(({ name }) => `display "${name}" photos: the list's records load with one set of photos, so name those of display "${shown[0].name}"`);
81
82
  };
83
+ const relatedColumnsErrors = (view, model, models) => (view.list?.relatedColumns ?? []).flatMap((column) => [
84
+ ...Object.hasOwn(model.fields, column.name) ? [`"${column.name}" is a field of ${model.name}`] : [],
85
+ ...view.list.columns.includes(column.name) ? [] : ["it is not among the list's columns"],
86
+ ...relatedColumnErrors(column, view.resource, models)
87
+ ].map((error) => `related column "${column.name}": ${error}`));
82
88
  const galleryErrors = (item, resource, model, models) => {
83
89
  const image = item.image === void 0 ? void 0 : model.fields[item.image];
84
90
  return [...image && image.type !== "file" ? [`"${item.image}" is not a file field`] : [], ...item.photos ? cardPhotosErrors(item.photos, resource, models).map((error) => `photos: ${error}`) : []];
@@ -92,7 +98,7 @@ const listOfErrors = (model, name, models) => {
92
98
  * Fails at startup when a view uses a sensitive field where it would show empty, a display or custom section names a
93
99
  * field the resource does not have, labels or colours values its enum does not have, shows a field with a field type of
94
100
  * `fieldTypes` (each with the storage types it shows) that does not show it,
95
- * has a related section, a cards display's or gallery's photos or a cards display's favorite button, a `listOf`, a "Save search" button or a
101
+ * has a related section or related list column, a cards display's or gallery's photos or a cards display's favorite button, a `listOf`, a "Save search" button or a
96
102
  * `searchesOf` that do not fit the resources, cards
97
103
  * displays of one list that name different photos, a map that cannot show the
98
104
  * record's position or a map display that cannot find the records in its view, a link to an address the app does not have, or a create page that is not one of `pages`.
@@ -103,7 +109,7 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
103
109
  const problems = views.flatMap((view) => {
104
110
  const model = models[view.resource];
105
111
  if (!model) return [];
106
- const related = view.layout.flatMap((item) => item.kind === "related" ? relatedErrors(item, view.resource, models).map((error) => `related "${item.title}": ${error}`) : []);
112
+ const related = [...view.layout.flatMap((item) => item.kind === "related" ? relatedErrors(item, view.resource, models).map((error) => `related "${item.title}": ${error}`) : []), ...relatedColumnsErrors(view, model, models)];
107
113
  const photos = [
108
114
  ...(view.list?.displays ?? []).flatMap((display) => display.kind === "cards" && display.photos ? cardPhotosErrors(display.photos, view.resource, models).map((error) => `display "${display.name}" photos: ${error}`) : []),
109
115
  ...sharedPhotosErrors(view),
@@ -2,6 +2,7 @@ import { relatedErrors } from "./view-related.js";
2
2
  import { cardPhotosErrors } from "./view-card-photos.js";
3
3
  import { cardFavoriteErrors } from "./view-card-favorite.js";
4
4
  import { searchesOfFor } from "./view-saved-search.js";
5
+ import { relatedColumnErrors } from "./view-related-column.js";
5
6
  import { resolveFieldPath } from "@protobase/layout";
6
7
  import { addressErrors, hrefPlaceholders, navRecentErrors, pruneByShape, shapes, tableDisplay } from "@protobase/schema/internal";
7
8
  //#region src/view-access.ts
@@ -22,20 +23,26 @@ const pruneGallery = (item, resource, model, models) => {
22
23
  kind: "gallery",
23
24
  title: item.title,
24
25
  ...image !== void 0 && { image },
25
- ...photos && { photos }
26
+ ...photos && { photos },
27
+ ...item.when !== void 0 && { when: item.when }
26
28
  };
27
29
  };
28
- const keepDefault = ({ default: opens, displays, ...list }) => {
30
+ const tidyList = (model) => ({ default: opens, displays, columns, relatedColumns, ...list }) => {
29
31
  const shown = displays && displays.length > 0 ? displays : void 0;
30
32
  const kept = opens === tableDisplay || shown?.some((display) => display.name === opens);
33
+ const related = relatedColumns && relatedColumns.length > 0 ? relatedColumns : void 0;
34
+ const visible = columns.filter((name) => Object.hasOwn(model.fields, name) || related?.some((column) => column.name === name));
31
35
  return {
32
36
  ...list,
37
+ columns: visible,
38
+ ...related && { relatedColumns: related },
33
39
  ...shown && { displays: shown },
34
40
  ...opens !== void 0 && kept && { default: opens }
35
41
  };
36
42
  };
37
- const listShape = (resource, models, prompted) => shapes.object({
38
- columns: shapes.list(shapes.field),
43
+ const listShape = (resource, model, models, prompted) => shapes.object({
44
+ columns: shapes.data,
45
+ relatedColumns: shapes.list(shapes.map((column) => relatedColumnErrors(column, resource, models).length === 0 ? column : void 0)),
39
46
  sort: shapes.list(shapes.tuple(shapes.field, shapes.data)),
40
47
  search: shapes.list(shapes.field),
41
48
  displays: shapes.list(shapes.oneOf({
@@ -81,7 +88,7 @@ const listShape = (resource, models, prompted) => shapes.object({
81
88
  }, { requires: ["props"] })
82
89
  })),
83
90
  default: shapes.data
84
- }, { after: keepDefault });
91
+ }, { after: tidyList(model) });
85
92
  const widgetShape = shapes.oneOf({
86
93
  range: shapes.object({
87
94
  kind: shapes.data,
@@ -115,11 +122,13 @@ const layoutShape = (resource, model, models) => shapes.oneOf({
115
122
  help: shapes.data,
116
123
  helpWhen: shapes.data,
117
124
  presentation: shapes.data,
125
+ when: shapes.data,
118
126
  fields: shapes.list(shapes.field, { nonEmpty: true })
119
127
  }, { requires: ["fields"] }),
120
128
  sidebar: shapes.object({
121
129
  kind: shapes.data,
122
- fields: shapes.list(shapes.field, { nonEmpty: true })
130
+ fields: shapes.list(shapes.field, { nonEmpty: true }),
131
+ when: shapes.data
123
132
  }, { requires: ["fields"] }),
124
133
  related: shapes.map((item) => pruneRelated(item, resource, models)),
125
134
  custom: shapes.object({
@@ -127,6 +136,7 @@ const layoutShape = (resource, model, models) => shapes.oneOf({
127
136
  component: shapes.data,
128
137
  title: shapes.data,
129
138
  sidebar: shapes.data,
139
+ when: shapes.data,
130
140
  props: shapes.refs
131
141
  }, { requires: ["props"] }),
132
142
  map: shapes.object({
@@ -135,7 +145,8 @@ const layoutShape = (resource, model, models) => shapes.oneOf({
135
145
  latitude: shapes.field,
136
146
  longitude: shapes.field,
137
147
  zoom: shapes.data,
138
- sidebar: shapes.data
148
+ sidebar: shapes.data,
149
+ when: shapes.data
139
150
  }, { requires: ["latitude", "longitude"] }),
140
151
  gallery: shapes.map((item) => pruneGallery(item, resource, model, models))
141
152
  });
@@ -178,7 +189,7 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
178
189
  icon: shapes.data,
179
190
  help: shapes.data,
180
191
  fields: shapes.keys(),
181
- list: listShape(resource, models, prompted),
192
+ list: listShape(resource, model, models, prompted),
182
193
  filters: shapes.list(widgetShape),
183
194
  layout: shapes.list(layoutShape(resource, model, models)),
184
195
  chart: shapes.object({
@@ -201,7 +212,7 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
201
212
  /**
202
213
  * The view with every reference to a field the caller cannot read removed: list columns, sort, search and displays,
203
214
  * field hints, filter widgets, record sections, maps, galleries and custom components' props, the chart, title and search result, the
204
- * actions that set or read one, and the sidebar's `recent` group. Nothing in it may name a hidden field. Related sections
215
+ * actions that set or read one, and the sidebar's `recent` group. Nothing in it may name a hidden field. Related sections, related list columns
205
216
  * and links are checked against `models`, the caller's view of every resource, and so are `searchesOf`, cards' favorite
206
217
  * buttons and the "Save search" button; the buttons stay for a visitor when their resource is one of `prompted`, those
207
218
  * that ask the visitor to sign in.
@@ -0,0 +1,25 @@
1
+ import { relatedErrors } from "./view-related.js";
2
+ //#region src/view-related-column.ts
3
+ const statusErrors = (target, name) => {
4
+ const field = Object.hasOwn(target.fields, name) ? target.fields[name] : void 0;
5
+ if (!field) return [`status: unknown field "${name}" on ${target.name}`];
6
+ if (field.type !== "enum") return [`status: "${name}" on ${target.name} is not an enum field`];
7
+ return field.sensitive ? [`status: "${name}" on ${target.name} is sensitive, so it cannot be shown`] : [];
8
+ };
9
+ /**
10
+ * What is wrong with a related records column of the list of `resource`, against these models; empty when it can be
11
+ * shown. It finds its records as a related section does. Against the full models it checks the config; against a
12
+ * caller's models, whether that caller can use it.
13
+ */
14
+ const relatedColumnErrors = (column, resource, models) => {
15
+ const { name: _, label: __, status, ...match } = column;
16
+ const errors = relatedErrors({
17
+ kind: "related",
18
+ title: column.name,
19
+ ...match
20
+ }, resource, models);
21
+ const target = models[column.resource];
22
+ return target ? [...errors, ...statusErrors(target, status)] : errors;
23
+ };
24
+ //#endregion
25
+ export { relatedColumnErrors };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.19.12",
3
+ "version": "0.19.14",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@better-auth/passkey": "^1.7.7",
33
- "@protobase/layout": "0.19.12",
34
- "@protobase/query": "0.19.12",
35
- "@protobase/schema": "0.19.12",
33
+ "@protobase/layout": "0.19.14",
34
+ "@protobase/query": "0.19.14",
35
+ "@protobase/schema": "0.19.14",
36
36
  "@react-email/render": "^2.1.0",
37
37
  "@scalar/hono-api-reference": "^0.12.10",
38
38
  "@sentry/core": "^11.7.0",