@protobase/server 0.19.11 → 0.19.13

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
@@ -25,17 +26,22 @@ const pruneGallery = (item, resource, model, models) => {
25
26
  ...photos && { photos }
26
27
  };
27
28
  };
28
- const keepDefault = ({ default: opens, displays, ...list }) => {
29
+ const tidyList = (model) => ({ default: opens, displays, columns, relatedColumns, ...list }) => {
29
30
  const shown = displays && displays.length > 0 ? displays : void 0;
30
31
  const kept = opens === tableDisplay || shown?.some((display) => display.name === opens);
32
+ const related = relatedColumns && relatedColumns.length > 0 ? relatedColumns : void 0;
33
+ const visible = columns.filter((name) => Object.hasOwn(model.fields, name) || related?.some((column) => column.name === name));
31
34
  return {
32
35
  ...list,
36
+ columns: visible,
37
+ ...related && { relatedColumns: related },
33
38
  ...shown && { displays: shown },
34
39
  ...opens !== void 0 && kept && { default: opens }
35
40
  };
36
41
  };
37
- const listShape = (resource, models, prompted) => shapes.object({
38
- columns: shapes.list(shapes.field),
42
+ const listShape = (resource, model, models, prompted) => shapes.object({
43
+ columns: shapes.data,
44
+ relatedColumns: shapes.list(shapes.map((column) => relatedColumnErrors(column, resource, models).length === 0 ? column : void 0)),
39
45
  sort: shapes.list(shapes.tuple(shapes.field, shapes.data)),
40
46
  search: shapes.list(shapes.field),
41
47
  displays: shapes.list(shapes.oneOf({
@@ -81,7 +87,7 @@ const listShape = (resource, models, prompted) => shapes.object({
81
87
  }, { requires: ["props"] })
82
88
  })),
83
89
  default: shapes.data
84
- }, { after: keepDefault });
90
+ }, { after: tidyList(model) });
85
91
  const widgetShape = shapes.oneOf({
86
92
  range: shapes.object({
87
93
  kind: shapes.data,
@@ -178,7 +184,7 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
178
184
  icon: shapes.data,
179
185
  help: shapes.data,
180
186
  fields: shapes.keys(),
181
- list: listShape(resource, models, prompted),
187
+ list: listShape(resource, model, models, prompted),
182
188
  filters: shapes.list(widgetShape),
183
189
  layout: shapes.list(layoutShape(resource, model, models)),
184
190
  chart: shapes.object({
@@ -201,7 +207,7 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
201
207
  /**
202
208
  * The view with every reference to a field the caller cannot read removed: list columns, sort, search and displays,
203
209
  * 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
210
+ * 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
211
  * and links are checked against `models`, the caller's view of every resource, and so are `searchesOf`, cards' favorite
206
212
  * buttons and the "Save search" button; the buttons stay for a visitor when their resource is one of `prompted`, those
207
213
  * 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.11",
3
+ "version": "0.19.13",
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.11",
34
- "@protobase/query": "0.19.11",
35
- "@protobase/schema": "0.19.11",
33
+ "@protobase/layout": "0.19.13",
34
+ "@protobase/query": "0.19.13",
35
+ "@protobase/schema": "0.19.13",
36
36
  "@react-email/render": "^2.1.0",
37
37
  "@scalar/hono-api-reference": "^0.12.10",
38
38
  "@sentry/core": "^11.7.0",