@protobase/server 0.19.15 → 0.19.16
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/better-auth/create-auth.d.ts +16 -1
- package/dist/better-auth/create-auth.js +8 -2
- package/dist/check-owners.js +15 -0
- package/dist/check-views.js +8 -2
- package/dist/create-admin.js +2 -0
- package/dist/owner-id.js +21 -0
- package/dist/registry.js +10 -0
- package/dist/request-access.js +2 -0
- package/dist/validate-body.js +14 -2
- package/dist/view-access.js +27 -3
- package/dist/view-editable.js +23 -0
- package/package.json +4 -4
|
@@ -77,6 +77,11 @@ export type CreateAuthOptions = {
|
|
|
77
77
|
encryptOAuthTokens?: boolean;
|
|
78
78
|
/** Runs after a user is created, by any route: `createUser`, an admin, or a social sign-up. Default none. */
|
|
79
79
|
onUserCreated?: UserCreatedHook;
|
|
80
|
+
/**
|
|
81
|
+
* The id of a new user, by any route. Default Better Auth's random 32-character text. `.owner()` fields hold it: a
|
|
82
|
+
* `text` column for the default, or an integer column when this takes the next value of the app's own users sequence.
|
|
83
|
+
*/
|
|
84
|
+
generateUserId?: () => string | Promise<string>;
|
|
80
85
|
/**
|
|
81
86
|
* The identity provider the operator's staff sign in with to sign in as people of this app, for support. Defaults
|
|
82
87
|
* to the one the platform passes in `PROTOBASE_OPERATOR_ISSUER`, `PROTOBASE_OPERATOR_CLIENT_ID` and
|
|
@@ -3744,7 +3749,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3744
3749
|
name?: string;
|
|
3745
3750
|
slug?: string;
|
|
3746
3751
|
logo?: string | null;
|
|
3747
|
-
metadata
|
|
3752
|
+
metadata?
|
|
3753
|
+
/**
|
|
3754
|
+
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
3755
|
+
* sign-in provider already resolved.
|
|
3756
|
+
*/
|
|
3757
|
+
/**
|
|
3758
|
+
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
3759
|
+
* sign-in provider already resolved.
|
|
3760
|
+
*/ : Record<string, any>;
|
|
3748
3761
|
[key: string]: any;
|
|
3749
3762
|
};
|
|
3750
3763
|
}>;
|
|
@@ -4354,6 +4367,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4354
4367
|
roleDefinitions: RoleDefinitions;
|
|
4355
4368
|
organizations: ResolvedOrganizations | undefined;
|
|
4356
4369
|
defaultRole: string | undefined;
|
|
4370
|
+
/** New users get their ids from `generateUserId` instead of Better Auth's random text. */
|
|
4371
|
+
generatesUserIds: boolean;
|
|
4357
4372
|
mail: boolean;
|
|
4358
4373
|
passwordReset: boolean;
|
|
4359
4374
|
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
@@ -163,10 +163,14 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
163
163
|
const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
164
164
|
const unknown = given.find((name) => !roles.includes(name));
|
|
165
165
|
if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
|
|
166
|
-
|
|
166
|
+
const withId = options.generateUserId ? {
|
|
167
167
|
...user,
|
|
168
|
+
id: await options.generateUserId()
|
|
169
|
+
} : user;
|
|
170
|
+
return { data: context?.path === "/admin/create-user" ? {
|
|
171
|
+
...withId,
|
|
168
172
|
emailVerified: true
|
|
169
|
-
} :
|
|
173
|
+
} : withId };
|
|
170
174
|
},
|
|
171
175
|
...options.onUserCreated && { after: options.onUserCreated }
|
|
172
176
|
} },
|
|
@@ -266,6 +270,8 @@ const createAuth = (options) => {
|
|
|
266
270
|
roleDefinitions: definitions,
|
|
267
271
|
organizations,
|
|
268
272
|
defaultRole,
|
|
273
|
+
/** New users get their ids from `generateUserId` instead of Better Auth's random text. */
|
|
274
|
+
generatesUserIds: Boolean(options.generateUserId),
|
|
269
275
|
mail: Boolean(mailer),
|
|
270
276
|
passwordReset: Boolean(mailer),
|
|
271
277
|
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/check-owners.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stops startup on an `.owner()` field that cannot hold the ids of the auth instance's users: Better Auth's own ids
|
|
4
|
+
* are random text, so the field must be text unless `generateUserId` makes the ids. Without an auth instance the
|
|
5
|
+
* ids come from the app's own authenticator, which this cannot see.
|
|
6
|
+
*/
|
|
7
|
+
const checkOwnerFields = (registry, auth) => {
|
|
8
|
+
if (!auth || auth.generatesUserIds) return;
|
|
9
|
+
for (const entry of registry.entries) {
|
|
10
|
+
if (entry.ownerType === void 0 || entry.ownerType === "text") continue;
|
|
11
|
+
throw new Error(`${entry.name}.${entry.model.owner} is the owner of its records, so it holds the signed-in user's id, but it holds ${entry.ownerType} values and Better Auth's user ids are random text: make it a text column, or give createAuth a generateUserId that makes ${entry.ownerType} ids`);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { checkOwnerFields };
|
package/dist/check-views.js
CHANGED
|
@@ -4,7 +4,8 @@ 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
6
|
import { relatedColumnErrors } from "./view-related-column.js";
|
|
7
|
-
import {
|
|
7
|
+
import { editableErrors } from "./view-editable.js";
|
|
8
|
+
import { actionInputErrors, addressErrors, addressPlaceholders, boardErrors, displayFields, fieldRefsIn, hrefField, hrefPlaceholders, linkActionError } from "@protobase/schema/internal";
|
|
8
9
|
//#region src/check-views.ts
|
|
9
10
|
const linkFields = (run) => run?.kind !== "link" ? [] : "to" in run ? addressPlaceholders(run.to) : hrefPlaceholders(run.href);
|
|
10
11
|
const sensitiveErrors = (view, model) => {
|
|
@@ -93,6 +94,7 @@ const relatedColumnsErrors = (view, model, models) => (view.list?.relatedColumns
|
|
|
93
94
|
...view.list.columns.includes(column.name) ? [] : ["it is not among the list's columns"],
|
|
94
95
|
...relatedColumnErrors(column, view.resource, models)
|
|
95
96
|
].map((error) => `related column "${column.name}": ${error}`));
|
|
97
|
+
const listEditErrors = (view, model) => (view.list?.editable ?? []).flatMap((name) => [...view.list.columns.includes(name) ? [] : ["it is not among the list's columns"], ...editableErrors(name, model, view.fields[name])].map((error) => `editable field "${name}": ${error}`));
|
|
96
98
|
const galleryErrors = (item, resource, model, models) => {
|
|
97
99
|
const image = item.image === void 0 ? void 0 : model.fields[item.image];
|
|
98
100
|
return [...image && image.type !== "file" ? [`"${item.image}" is not a file field`] : [], ...item.photos ? cardPhotosErrors(item.photos, resource, models).map((error) => `photos: ${error}`) : []];
|
|
@@ -106,10 +108,11 @@ const listOfErrors = (model, name, models) => {
|
|
|
106
108
|
* Fails at startup when a view uses a sensitive field where it would show empty, a display or custom section names a
|
|
107
109
|
* field the resource does not have, labels or colours values its enum does not have, shows a field with a field type of
|
|
108
110
|
* `fieldTypes` (each with the storage types it shows) that does not show it,
|
|
111
|
+
* edits a list column in its cell that is not a field with an editor of one line the API may write,
|
|
109
112
|
* 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
|
|
110
113
|
* `searchesOf` that do not fit the resources, cards
|
|
111
114
|
* displays of one list that name different photos, a map that cannot show the
|
|
112
|
-
* 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, action inputs that do not fit, or a create page that is not one of `pages`.
|
|
115
|
+
* record's position or a map display that cannot find the records in its view, a board whose columns are no filterable enum, a link to an address the app does not have, action inputs that do not fit, or a create page that is not one of `pages`.
|
|
113
116
|
*/
|
|
114
117
|
const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
115
118
|
const models = Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model]));
|
|
@@ -127,12 +130,14 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
|
127
130
|
const listOf = view.listOf === void 0 ? [] : listOfErrors(model, view.listOf, models).map((error) => `listOf: ${error}`);
|
|
128
131
|
const searches = [...view.saveSearch ? saveSearchErrors(view.saveSearch, view.resource, models, views).map((error) => `saveSearch: ${error}`) : [], ...view.searchesOf ? searchesOfErrors(view.searchesOf, view.resource, models).map((error) => `searchesOf: ${error}`) : []];
|
|
129
132
|
const maps = [...view.layout.flatMap((item) => item.kind === "map" ? mapErrors(item, model).map((error) => `map "${item.title}": ${error}`) : []), ...(view.list?.displays ?? []).flatMap((display) => display.kind === "map" ? mapDisplayErrors(display, model).map((error) => `display "${display.name}": ${error}`) : [])];
|
|
133
|
+
const boards = (view.list?.displays ?? []).flatMap((display) => display.kind === "board" ? boardErrors(display, model).map((error) => `display "${display.name}": ${error}`) : []);
|
|
130
134
|
return [
|
|
131
135
|
...sensitiveErrors(view, model),
|
|
132
136
|
...unknownFieldErrors(view, model),
|
|
133
137
|
...valueLabelErrors(view, model),
|
|
134
138
|
...valueColorErrors(view, model),
|
|
135
139
|
...textHintErrors(view, model),
|
|
140
|
+
...listEditErrors(view, model),
|
|
136
141
|
...widgetErrors(view, model, fieldTypes),
|
|
137
142
|
...related,
|
|
138
143
|
...photos,
|
|
@@ -140,6 +145,7 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
|
140
145
|
...listOf,
|
|
141
146
|
...searches,
|
|
142
147
|
...maps,
|
|
148
|
+
...boards,
|
|
143
149
|
...linkErrors(view, model, models, pageNames),
|
|
144
150
|
...inputErrors(view, models),
|
|
145
151
|
...createErrors(view, pageNames)
|
package/dist/create-admin.js
CHANGED
|
@@ -16,6 +16,7 @@ import { webhookTable } from "./webhooks/table/webhook-table.js";
|
|
|
16
16
|
import { subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
|
|
17
17
|
import { withAnonymous } from "./anonymous.js";
|
|
18
18
|
import { checkBottomBars, checkShell } from "./check-shell.js";
|
|
19
|
+
import { checkOwnerFields } from "./check-owners.js";
|
|
19
20
|
import { checkRoleLimits } from "./check-roles.js";
|
|
20
21
|
import { checkViews } from "./check-views.js";
|
|
21
22
|
import { errorReportingSettings } from "./error-reporting/error-reporting-settings.js";
|
|
@@ -59,6 +60,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
59
60
|
checkBottomBars(registry, bottomBarModels, pageModels.map((entry) => entry.name), declaredRoles(auth, options.roles));
|
|
60
61
|
checkViews(registry, viewModels, pageModels.map((entry) => entry.name), fieldTypes);
|
|
61
62
|
checkRoleLimits(registry, options.roles?.limits ?? []);
|
|
63
|
+
checkOwnerFields(registry, auth);
|
|
62
64
|
checkPages(pageModels, {
|
|
63
65
|
models: Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model])),
|
|
64
66
|
actions: (resource) => new Set(viewModels.filter((view) => view.resource === resource).flatMap((view) => view.actions.map((action) => action.name)))
|
package/dist/owner-id.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HttpProblem } from "./problem.js";
|
|
2
|
+
import { fitsKeyType } from "@protobase/schema";
|
|
3
|
+
//#region src/owner-id.ts
|
|
4
|
+
const mentions = (filter, name) => {
|
|
5
|
+
if (filter.kind === "and" || filter.kind === "or") return filter.args.some((arg) => mentions(arg, name));
|
|
6
|
+
if (filter.kind === "not") return mentions(filter.arg, name);
|
|
7
|
+
return "field" in filter && filter.field.name === name;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Refuses a request whose row filters compare the `.owner()` field with a user id it cannot hold, such as Better
|
|
11
|
+
* Auth's random text in an integer column, naming the field, instead of failing as a value of the wrong format.
|
|
12
|
+
*/
|
|
13
|
+
const checkOwnerId = (entry, user, resolved) => {
|
|
14
|
+
const { ownerType, model } = entry;
|
|
15
|
+
if (ownerType === void 0 || model.owner === void 0 || fitsKeyType(String(user.id), ownerType)) return;
|
|
16
|
+
const owner = model.owner;
|
|
17
|
+
if (!Object.values(resolved.rowFilter).some((filter) => filter && mentions(filter, owner))) return;
|
|
18
|
+
throw new HttpProblem(500, "owner-id-mismatch", "Internal Server Error", `${entry.name}.${owner} holds ${ownerType} user ids, and the signed-in user's id "${user.id}" is not one, so their own records cannot be found`);
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { checkOwnerId };
|
package/dist/registry.js
CHANGED
|
@@ -13,6 +13,14 @@ const etagSource = (model) => {
|
|
|
13
13
|
kind: "timestamp"
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
+
const isKeyType = (type) => type === "text" || type === "integer" || type === "bigint" || type === "uuid";
|
|
17
|
+
const ownerType = (model, resolve) => {
|
|
18
|
+
const field = model.owner === void 0 ? void 0 : model.fields[model.owner];
|
|
19
|
+
if (!field) return void 0;
|
|
20
|
+
if (isKeyType(field.type)) return field.type;
|
|
21
|
+
const target = field.type === "relation" && field.relation ? resolve(field.relation.resource) : void 0;
|
|
22
|
+
return target && keyTypes(target, resolve)[0];
|
|
23
|
+
};
|
|
16
24
|
const buildRegistry = (resources) => {
|
|
17
25
|
const built = resources.map((source) => ({
|
|
18
26
|
source,
|
|
@@ -25,11 +33,13 @@ const buildRegistry = (resources) => {
|
|
|
25
33
|
const types = keyTypes(model, (name) => models.get(name));
|
|
26
34
|
if (types.length !== model.primaryKey.length) throw new Error(`Not implemented: resource "${model.name}" has a primary key field that points at a composite-key resource; the server maps one key part per key field`);
|
|
27
35
|
const etag = etagSource(model);
|
|
36
|
+
const owner = ownerType(model, (name) => models.get(name));
|
|
28
37
|
return {
|
|
29
38
|
name: model.name,
|
|
30
39
|
source,
|
|
31
40
|
model,
|
|
32
41
|
keyTypes: types,
|
|
42
|
+
...owner && { ownerType: owner },
|
|
33
43
|
...etag && { etag }
|
|
34
44
|
};
|
|
35
45
|
});
|
package/dist/request-access.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { forbidden, notFound, unauthorized } from "./problem.js";
|
|
2
2
|
import { checkedFilter } from "./filter-input.js";
|
|
3
|
+
import { checkOwnerId } from "./owner-id.js";
|
|
3
4
|
import { tenantScope } from "./tenant.js";
|
|
4
5
|
import { checkFilter, resolveAccess, restrictModel } from "@protobase/schema";
|
|
5
6
|
//#region src/request-access.ts
|
|
@@ -16,6 +17,7 @@ const requestAccess = async (deps, entry, session) => {
|
|
|
16
17
|
},
|
|
17
18
|
...organizationOf(session)
|
|
18
19
|
}, deps.accessOptions);
|
|
20
|
+
checkOwnerId(entry, session.user, resolved);
|
|
19
21
|
const readable = restrictModel(entry.model, resolved);
|
|
20
22
|
return {
|
|
21
23
|
entry,
|
package/dist/validate-body.js
CHANGED
|
@@ -4,10 +4,13 @@ import { writableFields } from "./request-access.js";
|
|
|
4
4
|
const fail = (errors) => badRequest("invalid-record", `The request body is invalid: ${errors[0].field}: ${errors[0].message}`, { errors });
|
|
5
5
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
6
|
const reserved = ["etag", "permissions"];
|
|
7
|
+
const isEmptyText = (field, value) => field.type === "text" && !field.nullable && value === "";
|
|
7
8
|
/**
|
|
8
9
|
* Checks a create or update body for this caller. A field the caller cannot read is an unknown field, one they can read
|
|
9
10
|
* but not write is read-only (the tenant column and the API's own `etag` and `permissions` keys are always read-only);
|
|
10
|
-
* every provided value goes through its field's zod schema, then `.validate()` rules run on the result.
|
|
11
|
+
* every provided value goes through its field's zod schema, then `.validate()` rules run on the result. An empty text
|
|
12
|
+
* for a text field that may not be null counts as not given: a create fills in its default or reports it required,
|
|
13
|
+
* an update reports it required. Only the values written are checked, so a stored '' still reads.
|
|
11
14
|
* Returns only the values to write: on create the defaults are filled in, on update only the provided fields.
|
|
12
15
|
* `current` is the stored record an update merges into for `.validate()` rules.
|
|
13
16
|
*/
|
|
@@ -36,7 +39,16 @@ const validateBody = (access, mode, body, current = {}) => {
|
|
|
36
39
|
for (const [name, source] of Object.entries(entry.source.state.fields ?? {})) {
|
|
37
40
|
const field = full.fields[name];
|
|
38
41
|
if (!source || !field || !writable.has(name) || name === full.tenant) continue;
|
|
39
|
-
const
|
|
42
|
+
const empty = Object.hasOwn(body, name) && isEmptyText(field, body[name]);
|
|
43
|
+
if (empty && mode === "update") {
|
|
44
|
+
errors.push({
|
|
45
|
+
field: name,
|
|
46
|
+
code: "required",
|
|
47
|
+
message: `"${name}" is required`
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const provided = Object.hasOwn(body, name) && !empty;
|
|
40
52
|
if (!provided && mode === "update") continue;
|
|
41
53
|
if (!provided && field.default && "db" in field.default) continue;
|
|
42
54
|
const parsed = source.schema.safeParse(provided ? body[name] : void 0);
|
package/dist/view-access.js
CHANGED
|
@@ -27,15 +27,26 @@ const pruneGallery = (item, resource, model, models) => {
|
|
|
27
27
|
...item.when !== void 0 && { when: item.when }
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
30
|
+
const withKeptDots = (display, related = []) => {
|
|
31
|
+
if (display.kind !== "cards" || display.dots === void 0) return display;
|
|
32
|
+
const { dots, ...card } = display;
|
|
33
|
+
const kept = dots.filter((name) => related.some((column) => column.name === name));
|
|
34
|
+
return kept.length > 0 ? {
|
|
35
|
+
...card,
|
|
36
|
+
dots: kept
|
|
37
|
+
} : card;
|
|
38
|
+
};
|
|
39
|
+
const tidyList = (model) => ({ default: opens, displays, columns, relatedColumns, editable, ...list }) => {
|
|
33
40
|
const related = relatedColumns && relatedColumns.length > 0 ? relatedColumns : void 0;
|
|
41
|
+
const shown = displays && displays.length > 0 ? displays.map((display) => withKeptDots(display, related)) : void 0;
|
|
42
|
+
const kept = opens === tableDisplay || shown?.some((display) => display.name === opens);
|
|
34
43
|
const visible = columns.filter((name) => Object.hasOwn(model.fields, name) || related?.some((column) => column.name === name));
|
|
44
|
+
const writable = editable?.filter((name) => visible.includes(name) && !model.fields[name].readOnly);
|
|
35
45
|
return {
|
|
36
46
|
...list,
|
|
37
47
|
columns: visible,
|
|
38
48
|
...related && { relatedColumns: related },
|
|
49
|
+
...writable && writable.length > 0 && { editable: writable },
|
|
39
50
|
...shown && { displays: shown },
|
|
40
51
|
...opens !== void 0 && kept && { default: opens }
|
|
41
52
|
};
|
|
@@ -45,6 +56,7 @@ const listShape = (resource, model, models, prompted) => shapes.object({
|
|
|
45
56
|
relatedColumns: shapes.list(shapes.map((column) => relatedColumnErrors(column, resource, models).length === 0 ? column : void 0)),
|
|
46
57
|
sort: shapes.list(shapes.tuple(shapes.field, shapes.data)),
|
|
47
58
|
search: shapes.list(shapes.field),
|
|
59
|
+
editable: shapes.list(shapes.field),
|
|
48
60
|
displays: shapes.list(shapes.oneOf({
|
|
49
61
|
cards: shapes.object({
|
|
50
62
|
kind: shapes.data,
|
|
@@ -59,6 +71,7 @@ const listShape = (resource, model, models, prompted) => shapes.object({
|
|
|
59
71
|
meta: shapes.list(shapes.field),
|
|
60
72
|
footer: shapes.list(shapes.field),
|
|
61
73
|
flags: shapes.list(shapes.field),
|
|
74
|
+
dots: shapes.data,
|
|
62
75
|
card: shapes.data,
|
|
63
76
|
photos: shapes.map((photos) => cardPhotosErrors(photos, resource, models).length === 0 ? photos : void 0),
|
|
64
77
|
favorite: shapes.map((favorite) => prompted.has(favorite.resource) || cardFavoriteErrors(favorite, resource, models).length === 0 ? favorite : void 0)
|
|
@@ -77,6 +90,17 @@ const listShape = (resource, model, models, prompted) => shapes.object({
|
|
|
77
90
|
defaultRadius: shapes.data,
|
|
78
91
|
area: shapes.field
|
|
79
92
|
}, { requires: ["latitude", "longitude"] }),
|
|
93
|
+
board: shapes.object({
|
|
94
|
+
kind: shapes.data,
|
|
95
|
+
name: shapes.data,
|
|
96
|
+
label: shapes.data,
|
|
97
|
+
icon: shapes.data,
|
|
98
|
+
by: shapes.field,
|
|
99
|
+
title: shapes.field,
|
|
100
|
+
subtitle: shapes.field,
|
|
101
|
+
badge: shapes.field,
|
|
102
|
+
meta: shapes.list(shapes.field)
|
|
103
|
+
}, { requires: ["by"] }),
|
|
80
104
|
custom: shapes.object({
|
|
81
105
|
kind: shapes.data,
|
|
82
106
|
name: shapes.data,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/view-editable.ts
|
|
2
|
+
const cellTypes = /* @__PURE__ */ new Set([
|
|
3
|
+
"text",
|
|
4
|
+
"integer",
|
|
5
|
+
"bigint",
|
|
6
|
+
"decimal",
|
|
7
|
+
"date",
|
|
8
|
+
"enum",
|
|
9
|
+
"boolean",
|
|
10
|
+
"currency",
|
|
11
|
+
"country"
|
|
12
|
+
]);
|
|
13
|
+
/** What is wrong with editing the field `name` of the list in its cell; empty when it can be. */
|
|
14
|
+
const editableErrors = (name, model, hints) => {
|
|
15
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
16
|
+
if (!field) return [`"${name}" is not a field of ${model.name}`];
|
|
17
|
+
if (field.readOnly) return [`"${name}" is read-only`];
|
|
18
|
+
if (!cellTypes.has(field.type)) return [`"${name}" is a ${field.type} field, which is not edited in a cell`];
|
|
19
|
+
if (hints?.multiline || hints?.markdown) return [`"${name}" runs over several lines, which a cell does not hold`];
|
|
20
|
+
return [];
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { editableErrors };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.16",
|
|
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.
|
|
34
|
-
"@protobase/query": "0.19.
|
|
35
|
-
"@protobase/schema": "0.19.
|
|
33
|
+
"@protobase/layout": "0.19.16",
|
|
34
|
+
"@protobase/query": "0.19.16",
|
|
35
|
+
"@protobase/schema": "0.19.16",
|
|
36
36
|
"@react-email/render": "^2.1.0",
|
|
37
37
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
38
38
|
"@sentry/core": "^11.7.0",
|