@protobase/server 0.39.0 → 0.39.2
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.
- package/dist/meta-model.js +27 -0
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +5 -4
- package/dist/read-service.js +10 -2
- package/dist/request-access.js +6 -1
- package/dist/view-access.js +14 -8
- package/package.json +4 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { writeOnlyFields } from "./request-access.js";
|
|
2
|
+
//#region src/meta-model.ts
|
|
3
|
+
const writeOnly = (field) => ({
|
|
4
|
+
...field,
|
|
5
|
+
readOnly: false,
|
|
6
|
+
filterable: false,
|
|
7
|
+
sortable: false,
|
|
8
|
+
aliases: [],
|
|
9
|
+
sensitive: true,
|
|
10
|
+
writeOnly: true
|
|
11
|
+
});
|
|
12
|
+
/** The resource as `/meta` sends it to the caller: the fields they read, and their write-only ones marked `writeOnly`. */
|
|
13
|
+
const metaModel = (access) => {
|
|
14
|
+
const names = new Set(writeOnlyFields(access));
|
|
15
|
+
if (names.size === 0) return access.readable;
|
|
16
|
+
const fields = Object.entries(access.full.fields).flatMap(([name, field]) => {
|
|
17
|
+
if (names.has(name)) return [[name, writeOnly(field)]];
|
|
18
|
+
const readable = access.readable.fields[name];
|
|
19
|
+
return readable ? [[name, readable]] : [];
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
...access.readable,
|
|
23
|
+
fields: Object.fromEntries(fields)
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { metaModel };
|
package/dist/meta.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export type CallerMeta = {
|
|
|
60
60
|
};
|
|
61
61
|
/**
|
|
62
62
|
* `/meta` as one caller sees it: the resources they can do anything with, each as the restricted model (hidden fields
|
|
63
|
-
* absent, `readOnly` unless writable), the view for their roles with hidden fields pruned, what they may do, and the
|
|
63
|
+
* absent, `readOnly` unless writable, write-only ones marked), the view for their roles with hidden fields pruned, what they may do, and the
|
|
64
64
|
* user menu and their bottom bar without the resources they cannot see. A visitor also gets the prompts of the resources
|
|
65
65
|
* they could use once signed in, which keep their place in the bottom bar and the user menu.
|
|
66
66
|
* The ETag hashes all of it, so it differs per user and per roles.
|
package/dist/meta.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { requestAccess } from "./request-access.js";
|
|
1
|
+
import { requestAccess, writeOnlyFields } from "./request-access.js";
|
|
2
2
|
import { sha256Hex, stableJson } from "./hash.js";
|
|
3
3
|
import { seesAssistant } from "./assistant/assistant-settings.js";
|
|
4
4
|
import { pruneView } from "./view-access.js";
|
|
5
5
|
import { activeRole } from "./active-role.js";
|
|
6
6
|
import { pluginsFor } from "./plugins/plugin-settings.js";
|
|
7
|
+
import { metaModel } from "./meta-model.js";
|
|
7
8
|
import { seesRuntime } from "./runtime/runtime-settings.js";
|
|
8
9
|
import { sourcesFor } from "./sources/source-settings.js";
|
|
9
10
|
import { prunePage } from "@protobase/layout";
|
|
@@ -23,7 +24,7 @@ const signInView = (view) => {
|
|
|
23
24
|
const modelsHash = (deps) => sha256Hex(stableJson(deps.registry.entries.map((entry) => entry.model))).then((hash) => hash.slice(0, 32));
|
|
24
25
|
/**
|
|
25
26
|
* `/meta` as one caller sees it: the resources they can do anything with, each as the restricted model (hidden fields
|
|
26
|
-
* absent, `readOnly` unless writable), the view for their roles with hidden fields pruned, what they may do, and the
|
|
27
|
+
* absent, `readOnly` unless writable, write-only ones marked), the view for their roles with hidden fields pruned, what they may do, and the
|
|
27
28
|
* user menu and their bottom bar without the resources they cannot see. A visitor also gets the prompts of the resources
|
|
28
29
|
* they could use once signed in, which keep their place in the bottom bar and the user menu.
|
|
29
30
|
* The ETag hashes all of it, so it differs per user and per roles.
|
|
@@ -44,7 +45,7 @@ const createMeta = (deps, appName, appIcon) => {
|
|
|
44
45
|
}] : [];
|
|
45
46
|
});
|
|
46
47
|
const createPages = new Set(picked.filter(({ view }) => view.create).map(({ view }) => view.resource));
|
|
47
|
-
const views = picked.map(({ access, view }) => pruneView(view, access.readable, listed, prompted, createPages));
|
|
48
|
+
const views = picked.map(({ access, view }) => pruneView(view, access.readable, listed, prompted, createPages, new Set(writeOnlyFields(access))));
|
|
48
49
|
const callerAccess = {
|
|
49
50
|
createPages,
|
|
50
51
|
models: listed,
|
|
@@ -63,7 +64,7 @@ const createMeta = (deps, appName, appIcon) => {
|
|
|
63
64
|
const body = {
|
|
64
65
|
appName,
|
|
65
66
|
...appIcon && { appIcon },
|
|
66
|
-
resources: visible.map(
|
|
67
|
+
resources: visible.map(metaModel),
|
|
67
68
|
views,
|
|
68
69
|
pages,
|
|
69
70
|
permissions: Object.fromEntries(visible.map(({ entry, resolved, full }) => [entry.name, {
|
package/dist/read-service.js
CHANGED
|
@@ -6,8 +6,16 @@ import { fetchReadable } from "./fetch-readable.js";
|
|
|
6
6
|
import { rowPermissions } from "./row-permissions.js";
|
|
7
7
|
import { readTransaction } from "./transactions.js";
|
|
8
8
|
//#region src/read-service.ts
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `edit` for fields the caller may change on this record, `read` for the rest (sensitive ones included); hidden fields
|
|
11
|
+
* are not listed at all, nor is a write-only one unless the caller may change it here.
|
|
12
|
+
*/
|
|
13
|
+
const fieldModes = (access, canUpdate) => {
|
|
14
|
+
const writable = canUpdate ? writableFields(access, "update") : [];
|
|
15
|
+
const readable = Object.keys(access.readable.fields).map((name) => [name, writable.includes(name) ? "edit" : "read"]);
|
|
16
|
+
const writeOnly = writable.filter((name) => !Object.hasOwn(access.readable.fields, name)).map((name) => [name, "edit"]);
|
|
17
|
+
return Object.fromEntries([...readable, ...writeOnly]);
|
|
18
|
+
};
|
|
11
19
|
/**
|
|
12
20
|
* The record with this key as the caller may see it: readable fields only, inside the caller's tenant and row filter.
|
|
13
21
|
* A key outside the tenant, outside the row filter, or failing a record-level read rule is a plain 404.
|
package/dist/request-access.js
CHANGED
|
@@ -72,6 +72,11 @@ const writesBlind = (access, name, mode) => access.entry.source.fieldAccess()[na
|
|
|
72
72
|
* by the server.
|
|
73
73
|
*/
|
|
74
74
|
const writableFields = (access, mode) => access.resolved.writableFields[mode].filter((name) => (Object.hasOwn(access.readable.fields, name) || writesBlind(access, name, mode)) && name !== access.full.tenant);
|
|
75
|
+
/** The write-only fields: those the caller may write on create or on update but not read, in the model's order. */
|
|
76
|
+
const writeOnlyFields = (access) => {
|
|
77
|
+
const writable = /* @__PURE__ */ new Set([...writableFields(access, "create"), ...writableFields(access, "update")]);
|
|
78
|
+
return Object.keys(access.full.fields).filter((name) => writable.has(name) && !Object.hasOwn(access.readable.fields, name));
|
|
79
|
+
};
|
|
75
80
|
const visibleFields = (access) => Object.values(access.model.fields);
|
|
76
81
|
const andFilters = (filters) => {
|
|
77
82
|
const present = filters.filter((filter) => filter !== void 0);
|
|
@@ -135,4 +140,4 @@ const recordDecision = async (access, operation, record, input) => {
|
|
|
135
140
|
return asFilter(access, result, operation);
|
|
136
141
|
};
|
|
137
142
|
//#endregion
|
|
138
|
-
export { andFilters, readScope, recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields };
|
|
143
|
+
export { andFilters, readScope, recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields, writeOnlyFields };
|
package/dist/view-access.js
CHANGED
|
@@ -187,7 +187,13 @@ const widgetShape = shapes.oneOf({
|
|
|
187
187
|
secondary: shapes.data
|
|
188
188
|
}, { requires: ["fields"] })
|
|
189
189
|
});
|
|
190
|
-
const
|
|
190
|
+
const formFields = (model, writeOnly) => (name) => Object.hasOwn(model.fields, name) || writeOnly.has(name);
|
|
191
|
+
const sectionFields = (model, writeOnly) => shapes.map((names) => {
|
|
192
|
+
const kept = names.filter(formFields(model, writeOnly));
|
|
193
|
+
return kept.length > 0 ? kept : void 0;
|
|
194
|
+
});
|
|
195
|
+
const fieldHints = (model, writeOnly) => shapes.map((hints) => Object.fromEntries(Object.entries(hints).filter(([name]) => formFields(model, writeOnly)(name))));
|
|
196
|
+
const layoutShape = (resource, model, models, writeOnly) => {
|
|
191
197
|
const when = recordWhenShape(resource, models);
|
|
192
198
|
const section = shapes.object({
|
|
193
199
|
kind: shapes.data,
|
|
@@ -196,7 +202,7 @@ const layoutShape = (resource, model, models) => {
|
|
|
196
202
|
helpWhen: shapes.data,
|
|
197
203
|
presentation: shapes.data,
|
|
198
204
|
when,
|
|
199
|
-
fields:
|
|
205
|
+
fields: sectionFields(model, writeOnly)
|
|
200
206
|
}, { requires: ["fields", "when"] });
|
|
201
207
|
const related = shapes.map((item) => pruneRelated(item, resource, models));
|
|
202
208
|
const main = {
|
|
@@ -297,7 +303,7 @@ const actionShape = (model, models, createPages) => shapes.object({
|
|
|
297
303
|
"inputs",
|
|
298
304
|
"when"
|
|
299
305
|
] });
|
|
300
|
-
const viewShape = (resource, model, models, prompted, createPages) => {
|
|
306
|
+
const viewShape = (resource, model, models, prompted, createPages, writeOnly) => {
|
|
301
307
|
const nav = shapes.object({
|
|
302
308
|
hidden: shapes.data,
|
|
303
309
|
group: shapes.data,
|
|
@@ -317,10 +323,10 @@ const viewShape = (resource, model, models, prompted, createPages) => {
|
|
|
317
323
|
names: shapes.data,
|
|
318
324
|
icon: shapes.data,
|
|
319
325
|
help: shapes.data,
|
|
320
|
-
fields:
|
|
326
|
+
fields: fieldHints(model, writeOnly),
|
|
321
327
|
list: listShape(resource, model, models, prompted),
|
|
322
328
|
filters: shapes.list(widgetShape),
|
|
323
|
-
layout: shapes.list(layoutShape(resource, model, models)),
|
|
329
|
+
layout: shapes.list(layoutShape(resource, model, models, writeOnly)),
|
|
324
330
|
chart,
|
|
325
331
|
recordMode: shapes.data,
|
|
326
332
|
helpWhen: shapes.data,
|
|
@@ -352,10 +358,10 @@ const viewShape = (resource, model, models, prompted, createPages) => {
|
|
|
352
358
|
* and links are checked against `models`, the caller's view of every resource, and so are `searchesOf`, cards' favorite
|
|
353
359
|
* buttons and the "Save search" button; the buttons stay for a visitor when their resource is one of `prompted`, those
|
|
354
360
|
* that ask the visitor to sign in. A create link may give values of any name to a resource of `createPages`, whose view
|
|
355
|
-
* for the caller opens a create page.
|
|
361
|
+
* for the caller opens a create page. The caller's `writeOnly` fields stay in record sections and field hints only.
|
|
356
362
|
*/
|
|
357
|
-
const pruneView = (view, model, models, prompted = /* @__PURE__ */ new Set(), createPages = /* @__PURE__ */ new Set()) => {
|
|
358
|
-
const pruned = pruneByShape(view, viewShape(view.resource, model, models, prompted, createPages), (name) => Object.hasOwn(model.fields, name));
|
|
363
|
+
const pruneView = (view, model, models, prompted = /* @__PURE__ */ new Set(), createPages = /* @__PURE__ */ new Set(), writeOnly = /* @__PURE__ */ new Set()) => {
|
|
364
|
+
const pruned = pruneByShape(view, viewShape(view.resource, model, models, prompted, createPages, writeOnly), (name) => Object.hasOwn(model.fields, name));
|
|
359
365
|
if (!pruned) throw new Error(`The view of ${view.resource} is not an object`);
|
|
360
366
|
return pruned;
|
|
361
367
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
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": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@better-auth/passkey": "^1.7.12",
|
|
37
37
|
"@better-auth/sso": "^1.7.12",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
39
|
-
"@protobase/layout": "0.39.
|
|
40
|
-
"@protobase/query": "0.39.
|
|
41
|
-
"@protobase/schema": "0.39.
|
|
39
|
+
"@protobase/layout": "0.39.2",
|
|
40
|
+
"@protobase/query": "0.39.2",
|
|
41
|
+
"@protobase/schema": "0.39.2",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|