@protobase/server 0.38.0 → 0.39.0
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/batch/reorder.js +2 -1
- package/dist/openapi/schemas.js +2 -2
- package/dist/request-access.js +5 -3
- package/dist/validate-body.js +17 -12
- package/package.json +4 -4
package/dist/batch/reorder.js
CHANGED
|
@@ -3,7 +3,7 @@ import { tenantScope } from "../tenant.js";
|
|
|
3
3
|
import { requireOperation, targetFilter, writableFields } from "../request-access.js";
|
|
4
4
|
import { columnId, keyConditions, scopeConditions, tableId, whereClause } from "../sql-parts.js";
|
|
5
5
|
import { fetchRecord } from "../records.js";
|
|
6
|
-
import { validateBody } from "../validate-body.js";
|
|
6
|
+
import { refuseUnknownField, validateBody } from "../validate-body.js";
|
|
7
7
|
import { missing, shown } from "../write-shared.js";
|
|
8
8
|
import { applyUpdate, planUpdate } from "../write-update.js";
|
|
9
9
|
import { checkRankAfter } from "../rank-after.js";
|
|
@@ -50,6 +50,7 @@ const reorderIn = async (trx, deps, access, input) => {
|
|
|
50
50
|
const { entry, full } = access;
|
|
51
51
|
requireOperation(access, "update");
|
|
52
52
|
const field = full.fields[input.field];
|
|
53
|
+
if (!Object.hasOwn(access.readable.fields, input.field)) refuseUnknownField(input.field);
|
|
53
54
|
validateBody(access, "update", { [input.field]: 1 });
|
|
54
55
|
if (field?.type !== "integer" && field?.type !== "bigint") throw badRequest("invalid-parameter", `"${input.field}" must be an integer field to order by`);
|
|
55
56
|
if (!writableFields(access, "update").includes(input.field)) throw badRequest("invalid-parameter", `"${input.field}" is read-only`);
|
package/dist/openapi/schemas.js
CHANGED
|
@@ -69,7 +69,7 @@ const only = (schema, names) => ({
|
|
|
69
69
|
});
|
|
70
70
|
/**
|
|
71
71
|
* Component schemas of a resource for one caller: the record with the fields they may read, and the create and update
|
|
72
|
-
* bodies with the fields they may write. Hidden fields appear nowhere.
|
|
72
|
+
* bodies with the fields they may write. Hidden fields appear nowhere; a write-only one only in the bodies.
|
|
73
73
|
*/
|
|
74
74
|
const resourceSchemas = (access) => {
|
|
75
75
|
const { entry, model, resolved } = access;
|
|
@@ -104,7 +104,7 @@ const resourceSchemas = (access) => {
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
record.required = [...record.required ?? [], "etag"];
|
|
107
|
-
const writable = access.
|
|
107
|
+
const writable = access.full;
|
|
108
108
|
const body = (names) => {
|
|
109
109
|
const properties = only(input.schema, names).properties;
|
|
110
110
|
const optional = new Set(names.filter((field) => writable.fields[field]?.nullable || writable.fields[field]?.default && "db" in writable.fields[field].default));
|
package/dist/request-access.js
CHANGED
|
@@ -65,11 +65,13 @@ const requireOperation = (access, operation) => {
|
|
|
65
65
|
if (access.session.anonymous) throw unauthorized(`Send a bearer token to ${operation} ${access.entry.name}`);
|
|
66
66
|
throw forbidden("access-denied", `You may not ${operation} ${access.entry.name}`);
|
|
67
67
|
};
|
|
68
|
+
const writesBlind = (access, name, mode) => access.entry.source.fieldAccess()[name]?.[mode] !== void 0 && !access.full.fields[name]?.file && name !== access.full.position?.field;
|
|
68
69
|
/**
|
|
69
|
-
* Fields the caller may write on create or update
|
|
70
|
-
*
|
|
70
|
+
* Fields the caller may write on create or update: those they read (a sensitive field counts as read), and write-only
|
|
71
|
+
* ones, which they cannot read but whose own write rule allows them (see `writesBlind`). The tenant column is always set
|
|
72
|
+
* by the server.
|
|
71
73
|
*/
|
|
72
|
-
const writableFields = (access, mode) => access.resolved.writableFields[mode].filter((name) => Object.hasOwn(access.readable.fields, name) && name !== access.full.tenant);
|
|
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);
|
|
73
75
|
const visibleFields = (access) => Object.values(access.model.fields);
|
|
74
76
|
const andFilters = (filters) => {
|
|
75
77
|
const present = filters.filter((filter) => filter !== void 0);
|
package/dist/validate-body.js
CHANGED
|
@@ -17,6 +17,15 @@ const invalidValue = (field, issue) => {
|
|
|
17
17
|
...params && { params }
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
|
+
const unknownField = (name) => ({
|
|
21
|
+
field: name,
|
|
22
|
+
code: "unknown-field",
|
|
23
|
+
message: `Unknown field "${name}"`
|
|
24
|
+
});
|
|
25
|
+
/** Refuses a field the caller may not name in this request as `validateBody` refuses an unknown one. */
|
|
26
|
+
const refuseUnknownField = (name) => {
|
|
27
|
+
throw fail([unknownField(name)]);
|
|
28
|
+
};
|
|
20
29
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
30
|
const reserved = ["etag", "permissions"];
|
|
22
31
|
const isEmptyText = (field, value) => field.type === "text" && !field.nullable && value === "";
|
|
@@ -26,11 +35,11 @@ const isEmptyText = (field, value) => field.type === "text" && !field.nullable &
|
|
|
26
35
|
*/
|
|
27
36
|
const requiredOnCreate = (field, schema) => !(field.default && "db" in field.default) && !field.nullable && !schema.safeParse(void 0).success;
|
|
28
37
|
/**
|
|
29
|
-
* Checks a create or update body for this caller. A field the caller
|
|
30
|
-
* but not write is read-only (the tenant column and the API's own `etag` and `permissions` keys are always
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* an update reports it required. Only the values written are checked, so a stored '' still reads.
|
|
38
|
+
* Checks a create or update body for this caller. A field the caller can neither read nor write is an unknown field, one
|
|
39
|
+
* they can read but not write is read-only (the tenant column and the API's own `etag` and `permissions` keys are always
|
|
40
|
+
* read-only), and a write-only one (see `writableFields`) is written like any other; every provided value goes through
|
|
41
|
+
* its field's zod schema, then `.validate()` rules run on the result. An empty text for a text field that may not be null
|
|
42
|
+
* counts as not given: a create fills in its default or reports it required, an update reports it required. Only the values written are checked, so a stored '' still reads.
|
|
34
43
|
* Returns only the values to write: on create the defaults are filled in, on update only the provided fields.
|
|
35
44
|
* `current` is the stored record an update merges into for `.validate()` rules, which get a set encrypted field of it as
|
|
36
45
|
* `encrypted`: they see an encrypted value as text only when this write gives it.
|
|
@@ -46,11 +55,7 @@ const validateBody = (access, mode, body, current = {}) => {
|
|
|
46
55
|
code: "read-only",
|
|
47
56
|
message: name === "etag" ? "The version is not part of the body; send it in the If-Match header" : `"${name}" is read-only`
|
|
48
57
|
});
|
|
49
|
-
else if (!Object.hasOwn(model.fields, name)) errors.push(
|
|
50
|
-
field: name,
|
|
51
|
-
code: "unknown-field",
|
|
52
|
-
message: `Unknown field "${name}"`
|
|
53
|
-
});
|
|
58
|
+
else if (!Object.hasOwn(model.fields, name) && !writable.has(name)) errors.push(unknownField(name));
|
|
54
59
|
else if (!writable.has(name) || name === full.tenant) errors.push({
|
|
55
60
|
field: name,
|
|
56
61
|
code: "read-only",
|
|
@@ -92,11 +97,11 @@ const validateBody = (access, mode, body, current = {}) => {
|
|
|
92
97
|
};
|
|
93
98
|
const issues = entry.source.validators.flatMap((validate) => validate(merged) ?? []);
|
|
94
99
|
if (issues.length > 0) throw fail(issues.map((issue) => ({
|
|
95
|
-
field: Object.hasOwn(model.fields, issue.field) ? issue.field : "_record",
|
|
100
|
+
field: Object.hasOwn(model.fields, issue.field) || writable.has(issue.field) ? issue.field : "_record",
|
|
96
101
|
code: "invalid-record",
|
|
97
102
|
message: issue.message
|
|
98
103
|
})));
|
|
99
104
|
return values;
|
|
100
105
|
};
|
|
101
106
|
//#endregion
|
|
102
|
-
export { requiredOnCreate, validateBody };
|
|
107
|
+
export { refuseUnknownField, requiredOnCreate, validateBody };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
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.
|
|
40
|
-
"@protobase/query": "0.
|
|
41
|
-
"@protobase/schema": "0.
|
|
39
|
+
"@protobase/layout": "0.39.0",
|
|
40
|
+
"@protobase/query": "0.39.0",
|
|
41
|
+
"@protobase/schema": "0.39.0",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|