@protobase/server 0.27.0 → 0.28.1
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/app-identity.d.ts +12 -0
- package/dist/app-name.js +4 -2
- package/dist/better-auth/create-auth.d.ts +6 -5
- package/dist/better-auth/create-auth.js +3 -4
- package/dist/check-parents.js +16 -0
- package/dist/comments/comment-access.js +1 -1
- package/dist/create-admin.d.ts +10 -1
- package/dist/create-admin.js +6 -3
- package/dist/error-handler.js +1 -0
- package/dist/fetch-readable.js +33 -0
- package/dist/history/read-history.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/meta.js +4 -3
- package/dist/parent-access.js +84 -0
- package/dist/project-config.d.ts +3 -0
- package/dist/project-config.js +4 -2
- package/dist/read-service.js +3 -29
- package/dist/records.js +2 -1
- package/dist/reveal-service.js +1 -1
- package/dist/row-permissions.js +19 -2
- package/dist/web-app/app-icons.d.ts +31 -0
- package/dist/web-app/app-icons.js +26 -0
- package/dist/web-app/web-manifest-route.js +2 -2
- package/dist/web-app/web-manifest.js +22 -3
- package/dist/write-pipeline.js +5 -1
- package/dist/write-shared.js +13 -1
- package/dist/write-update.js +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/app-identity.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `app` section of `protobase.config.ts`: the colours the installed app shows, beside its name (`createAuth`'s
|
|
4
|
+
* `appName`) and its icons (`protobase.favicon.svg` and the PNGs beside it). Any CSS colour, such as `#0d9488`.
|
|
5
|
+
*/
|
|
6
|
+
export type AppIdentity = {
|
|
7
|
+
/** The colour of the installed app's title bar and of the browser's toolbar around it. */
|
|
8
|
+
themeColor?: string;
|
|
9
|
+
/** The colour of the screen an installed app shows while it starts. */
|
|
10
|
+
backgroundColor?: string;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
package/dist/app-name.js
CHANGED
|
@@ -4,8 +4,10 @@ const appNameVariable = "PROTOBASE_APP_NAME";
|
|
|
4
4
|
const defaultAppName = "Protobase";
|
|
5
5
|
/**
|
|
6
6
|
* The app's name the platform passes in `PROTOBASE_APP_NAME`, such as `Property Finder`; `undefined` when it is not
|
|
7
|
-
* set. Protobase names the app with it in sign-in prompts and in the mail it sends,
|
|
7
|
+
* set. Protobase names the app with it in sign-in prompts and in the mail it sends, over the name the app gives itself.
|
|
8
8
|
*/
|
|
9
9
|
const readAppName = (env) => env["PROTOBASE_APP_NAME"]?.trim() || void 0;
|
|
10
|
+
/** The name the app goes by: the platform's `PROTOBASE_APP_NAME`, which deployments rename it with, over its own `own`. */
|
|
11
|
+
const appNameOver = (env, own) => readAppName(env) ?? own;
|
|
10
12
|
//#endregion
|
|
11
|
-
export { appNameVariable, defaultAppName, readAppName };
|
|
13
|
+
export { appNameOver, appNameVariable, defaultAppName, readAppName };
|
|
@@ -34,8 +34,9 @@ export type CreateAuthOptions = {
|
|
|
34
34
|
passkeyDomain?: string;
|
|
35
35
|
/**
|
|
36
36
|
* The app's name: authenticator apps and passkey prompts show it, the auth emails name the app with it, and the
|
|
37
|
-
* default mailer sends them from it, as the sender's display name
|
|
38
|
-
* `PROTOBASE_APP_NAME`, when set,
|
|
37
|
+
* default mailer sends them from it, as the sender's display name; the tab titles, the sidebar's logo and the
|
|
38
|
+
* installed app show it too. The name the platform passes in `PROTOBASE_APP_NAME`, when set, overrides it; without
|
|
39
|
+
* either it is `Protobase` (without a sender's display name).
|
|
39
40
|
*/
|
|
40
41
|
appName?: string;
|
|
41
42
|
/** Signs cookies and encrypts the JWT private keys. Generate with `openssl rand -base64 32`. */
|
|
@@ -1294,7 +1295,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
1294
1295
|
email: string;
|
|
1295
1296
|
emailVerified: boolean;
|
|
1296
1297
|
name: string;
|
|
1297
|
-
image
|
|
1298
|
+
image? /** Signs cookies and encrypts the JWT private keys. Generate with `openssl rand -base64 32`. */ : string | null | undefined;
|
|
1298
1299
|
};
|
|
1299
1300
|
};
|
|
1300
1301
|
}>>[];
|
|
@@ -4018,7 +4019,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4018
4019
|
userId: string;
|
|
4019
4020
|
expiresAt: Date;
|
|
4020
4021
|
token: string;
|
|
4021
|
-
ipAddress
|
|
4022
|
+
ipAddress?: string | null | undefined;
|
|
4022
4023
|
userAgent?: string | null | undefined;
|
|
4023
4024
|
};
|
|
4024
4025
|
user: Record<string, any> & {
|
|
@@ -5971,7 +5972,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
5971
5972
|
/** The app keeps each user's metadata, in the column `users.metadata` names. */
|
|
5972
5973
|
userMetadata: boolean;
|
|
5973
5974
|
socialProviders: string[];
|
|
5974
|
-
/** The app's name it signs in and mails as: `
|
|
5975
|
+
/** The app's name it signs in and mails as: `PROTOBASE_APP_NAME`, else `appName`, else `Protobase`. */
|
|
5975
5976
|
appName: string;
|
|
5976
5977
|
operator: string | undefined;
|
|
5977
5978
|
platformSignIn: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { appOrigins, multiHostBaseURL, readOrigins, resolveOrigins } from "./origins.js";
|
|
2
|
-
import {
|
|
2
|
+
import { appNameOver, defaultAppName } from "../app-name.js";
|
|
3
3
|
import { goTrueIssuer, goTrueKeyPair } from "../gotrue/keys.js";
|
|
4
4
|
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
5
5
|
import { narrowClaims } from "./roles/active-role.js";
|
|
@@ -38,7 +38,6 @@ const platformOperator = () => readOperatorSettings(globalThis.process?.env ?? {
|
|
|
38
38
|
const platformSignInSettings = () => readPlatformSignIn(globalThis.process?.env ?? {});
|
|
39
39
|
const platformOrigins = () => readOrigins(globalThis.process?.env ?? {});
|
|
40
40
|
const platformPasskeyDomain = () => readPasskeyDomain(globalThis.process?.env ?? {});
|
|
41
|
-
const platformAppName = () => readAppName(globalThis.process?.env ?? {});
|
|
42
41
|
const tokenLifetime = "15m";
|
|
43
42
|
/**
|
|
44
43
|
* Better Auth for a single-tenant admin: sign-in with a password, an emailed code or a passkey, two-factor
|
|
@@ -276,7 +275,7 @@ const createAuth = (options) => {
|
|
|
276
275
|
const signUpRole = options.signUp && checkSignUpRole(options.signUp.role, roles);
|
|
277
276
|
const others = roles.filter((role) => role !== "admin");
|
|
278
277
|
const defaultRole = options.defaultRole ?? (options.organizations ? "user" : others.length === 1 ? others[0] : void 0);
|
|
279
|
-
const appName =
|
|
278
|
+
const appName = appNameOver(globalThis.process?.env ?? {}, options.appName);
|
|
280
279
|
const mailer = options.mailer === void 0 ? platformMailer(void 0, appName) : options.mailer || void 0;
|
|
281
280
|
const provider = options.operator === void 0 ? platformOperator() : options.operator || void 0;
|
|
282
281
|
const operator = provider && resolveOperator(provider);
|
|
@@ -329,7 +328,7 @@ const createAuth = (options) => {
|
|
|
329
328
|
/** The app keeps each user's metadata, in the column `users.metadata` names. */
|
|
330
329
|
userMetadata: Boolean(options.users?.metadata),
|
|
331
330
|
socialProviders,
|
|
332
|
-
/** The app's name it signs in and mails as: `
|
|
331
|
+
/** The app's name it signs in and mails as: `PROTOBASE_APP_NAME`, else `appName`, else `Protobase`. */
|
|
333
332
|
appName: appName ?? "Protobase",
|
|
334
333
|
operator: operator?.name,
|
|
335
334
|
platformSignIn: platformSignIn && {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/check-parents.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stops startup on a `.parent()` whose relation cannot find the parent record: a resource the app does not have, or one
|
|
4
|
+
* whose key is not exactly one field, the relation's one column.
|
|
5
|
+
*/
|
|
6
|
+
const checkParents = (registry) => {
|
|
7
|
+
const problems = registry.entries.flatMap(({ name, model }) => (model.parents ?? []).flatMap(({ field }) => {
|
|
8
|
+
const resource = model.fields[field].relation.resource;
|
|
9
|
+
const parent = registry.find(resource)?.model;
|
|
10
|
+
if (!parent) return [`${name} parent "${field}": unknown resource "${resource}"`];
|
|
11
|
+
return parent.primaryKey.length === 1 ? [] : [`${name} parent "${field}": "${resource}" has a key of ${parent.primaryKey.length} fields, not one`];
|
|
12
|
+
}));
|
|
13
|
+
if (problems.length > 0) throw new Error(`The parents cannot find the records they point at:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { checkParents };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { forbidden, notFound } from "../problem.js";
|
|
2
2
|
import { requestAccess, requireOperation } from "../request-access.js";
|
|
3
|
-
import { fetchReadable } from "../
|
|
3
|
+
import { fetchReadable } from "../fetch-readable.js";
|
|
4
4
|
//#region src/comments/comment-access.ts
|
|
5
5
|
/** The caller's access to a resource with comments, who must be allowed to read it; a resource without is a 404. */
|
|
6
6
|
const commentAccess = async (deps, entry, session) => {
|
package/dist/create-admin.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { MobileConfig } from "./mobile-config.js";
|
|
2
|
+
import { AppIdentity } from "./app-identity.js";
|
|
3
|
+
import { AppIcons } from "./web-app/app-icons.js";
|
|
2
4
|
import { AdminAuth } from "./better-auth/create-auth.js";
|
|
3
5
|
import { EventsOptions } from "./changes/events-options.js";
|
|
4
6
|
import { JobDefinition } from "./jobs/job-types.js";
|
|
@@ -46,6 +48,13 @@ export type CreateAdminInput = {
|
|
|
46
48
|
webhooks?: WebhooksOptions;
|
|
47
49
|
/** The app's iOS and Android build (see `MobileConfig`): the API answers its Capacitor origins through CORS. */
|
|
48
50
|
mobile?: MobileConfig;
|
|
51
|
+
/** The app's colours, which its web manifest carries (see `AppIdentity`). */
|
|
52
|
+
app?: AppIdentity;
|
|
53
|
+
/**
|
|
54
|
+
* The icons the project has of its own, as `protobase build` and `protobase dev` find them beside its favicon: the
|
|
55
|
+
* web manifest names its PNGs, and `/meta` the favicon when it is the project's, which the sidebar's logo draws.
|
|
56
|
+
*/
|
|
57
|
+
icons?: AppIcons;
|
|
49
58
|
/**
|
|
50
59
|
* The plugins the app's bundle carries, as `protobase build` writes them: its protobase.ui.tsx and its extensions'
|
|
51
60
|
* UI configs, each a plugin.json path on the app's origin. `/meta` names them to everyone, before any other plugin,
|
|
@@ -72,5 +81,5 @@ export type CreateAdminInput = {
|
|
|
72
81
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
73
82
|
};
|
|
74
83
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
75
|
-
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, mobile, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
84
|
+
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
76
85
|
//#endregion
|
package/dist/create-admin.js
CHANGED
|
@@ -41,6 +41,7 @@ import { visitorsBrowse, withAnonymous } from "./anonymous.js";
|
|
|
41
41
|
import { checkBottomBars, checkShell } from "./check-shell.js";
|
|
42
42
|
import { checkOwnerFields } from "./check-owners.js";
|
|
43
43
|
import { checkRoleLimits } from "./check-roles.js";
|
|
44
|
+
import { checkParents } from "./check-parents.js";
|
|
44
45
|
import { checkPositionAfter } from "./check-position-after.js";
|
|
45
46
|
import { checkViews } from "./check-views.js";
|
|
46
47
|
import { liveRoutes } from "./live/live-route.js";
|
|
@@ -63,6 +64,7 @@ import { batchRoute } from "./routes/batch.js";
|
|
|
63
64
|
import { collectionRoutes } from "./routes/collection.js";
|
|
64
65
|
import { docsRoutes } from "./routes/docs.js";
|
|
65
66
|
import { metaRoutes } from "./routes/meta.js";
|
|
67
|
+
import { faviconPath } from "./web-app/web-manifest.js";
|
|
66
68
|
import { webManifestRoute } from "./web-app/web-manifest-route.js";
|
|
67
69
|
import { recordRoutes } from "./routes/record.js";
|
|
68
70
|
import { createScanGuard } from "./scan-guard.js";
|
|
@@ -75,7 +77,7 @@ const declaredRoles = (auth, roles) => {
|
|
|
75
77
|
return names.length > 0 ? [...new Set(names)] : void 0;
|
|
76
78
|
};
|
|
77
79
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
78
|
-
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, mobile, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
80
|
+
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
79
81
|
const basePath = options.basePath ?? "/api/v1";
|
|
80
82
|
const appModels = appResources.map((resource) => resource.toModel());
|
|
81
83
|
const withComments = appModels.some((model) => model.comments);
|
|
@@ -110,6 +112,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
110
112
|
checkRoleLimits(registry, options.roles?.limits ?? []);
|
|
111
113
|
checkOwnerFields(registry, auth);
|
|
112
114
|
checkPositionAfter(registry);
|
|
115
|
+
checkParents(registry);
|
|
113
116
|
checkPages(pageModels, {
|
|
114
117
|
models: Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model])),
|
|
115
118
|
actions: (resource) => new Set(viewModels.filter((view) => view.resource === resource).flatMap((view) => view.actions.map((action) => action.name))),
|
|
@@ -191,7 +194,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
191
194
|
} }
|
|
192
195
|
};
|
|
193
196
|
const appName = auth?.appName ?? readAppName(globalThis.process?.env ?? {}) ?? "Protobase";
|
|
194
|
-
const meta = createMeta(deps, appName);
|
|
197
|
+
const meta = createMeta(deps, appName, icons?.favicon ? faviconPath : void 0);
|
|
195
198
|
const onError = createErrorHandler(options.onUnhandledError);
|
|
196
199
|
const guarded = (routes, caller = authenticate) => {
|
|
197
200
|
const app = new Hono();
|
|
@@ -301,7 +304,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
301
304
|
}
|
|
302
305
|
app.route(basePath, api);
|
|
303
306
|
app.route(`${deps.systemPath}/meta`, metaApp);
|
|
304
|
-
app.route(systemPath || "/", webManifestRoute(appName));
|
|
307
|
+
app.route(systemPath || "/", webManifestRoute(appName, identity, icons));
|
|
305
308
|
app.route(deps.systemPath || "/", system);
|
|
306
309
|
const webhooks = createWebhooks({
|
|
307
310
|
deps,
|
package/dist/error-handler.js
CHANGED
|
@@ -28,6 +28,7 @@ const databaseProblem = (error) => {
|
|
|
28
28
|
if (error.code === "57014") return new HttpProblem(504, "statement-timeout", "Gateway Timeout", "The query exceeded the statement timeout");
|
|
29
29
|
if (error.code === "23505") return new HttpProblem(409, "conflict", "Conflict", "A record with the same unique value already exists");
|
|
30
30
|
if (error.code === "23503") return new HttpProblem(409, "conflict", "Conflict", "The record points at, or is pointed at by, another record");
|
|
31
|
+
if (error.code === "40P01") return new HttpProblem(409, "conflict", "Conflict", "The record was written at the same time as another; send it again");
|
|
31
32
|
if (error.code === "23502") return new HttpProblem(400, "invalid-record", "Bad Request", "A required column has no value");
|
|
32
33
|
if (error.code === "23514") return new HttpProblem(400, "invalid-record", "Bad Request", "A check constraint rejected the record");
|
|
33
34
|
if (error.code.startsWith("22")) return new HttpProblem(400, "invalid-value", "Bad Request", "A value has the wrong format for its column");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { tenantScope } from "./tenant.js";
|
|
2
|
+
import { recordDecision, targetFilter } from "./request-access.js";
|
|
3
|
+
import { fetchRecord } from "./records.js";
|
|
4
|
+
//#region src/fetch-readable.ts
|
|
5
|
+
/**
|
|
6
|
+
* The row with this key if the caller may read it: inside their tenant and row filter, and passing a record-level read
|
|
7
|
+
* rule. Such a rule may look at fields the caller cannot see, so then the whole row is read; the caller cuts it down.
|
|
8
|
+
* `lock` locks the row the first read finds.
|
|
9
|
+
*/
|
|
10
|
+
const fetchReadable = async (trx, access, key, deleted, { fields, lock, ...shape }) => {
|
|
11
|
+
const { entry } = access;
|
|
12
|
+
const tenant = tenantScope(entry, access.session)?.tenantValue;
|
|
13
|
+
const recordRule = access.resolved.recordChecks.includes("read");
|
|
14
|
+
const filter = targetFilter(access, "read");
|
|
15
|
+
const current = await fetchRecord(trx, entry, key, tenant, {
|
|
16
|
+
deleted,
|
|
17
|
+
...filter && { filter },
|
|
18
|
+
...lock && { lock },
|
|
19
|
+
...shape,
|
|
20
|
+
...!recordRule && fields && { fields }
|
|
21
|
+
});
|
|
22
|
+
if (!current || !recordRule) return current;
|
|
23
|
+
const decision = await recordDecision(access, "read", current.record);
|
|
24
|
+
if (decision === false) return void 0;
|
|
25
|
+
if (decision !== true && !await fetchRecord(trx, entry, key, tenant, {
|
|
26
|
+
deleted,
|
|
27
|
+
filter: decision,
|
|
28
|
+
fields: [access.full.fields[access.full.primaryKey[0]]]
|
|
29
|
+
})) return void 0;
|
|
30
|
+
return current;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { fetchReadable };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { notFound } from "../problem.js";
|
|
2
2
|
import { tenantScope } from "../tenant.js";
|
|
3
3
|
import { requestAccess, requireOperation } from "../request-access.js";
|
|
4
|
+
import { fetchReadable } from "../fetch-readable.js";
|
|
4
5
|
import { readTransaction } from "../transactions.js";
|
|
5
|
-
import { fetchReadable } from "../read-service.js";
|
|
6
6
|
import { quoteIdent } from "../changes/capture-sql.js";
|
|
7
7
|
import { parseParams } from "../params.js";
|
|
8
8
|
import { encodeKey } from "@protobase/schema/internal";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { MobileConfig } from "./mobile-config.js";
|
|
2
|
+
import { AppIdentity } from "./app-identity.js";
|
|
3
|
+
import { AppIcons, PngIcon, pngIcons } from "./web-app/app-icons.js";
|
|
2
4
|
import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
|
|
3
5
|
import { StaffSignIn } from "./better-auth/staff-log.js";
|
|
4
6
|
import { MailMessage, Mailer } from "./mail/smtp-mailer.js";
|
|
@@ -98,4 +100,4 @@ import { isSchedule } from "./jobs/schedule.js";
|
|
|
98
100
|
import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
|
|
99
101
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
100
102
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
101
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
103
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ import { beforeSignInPluginsVariable, pluginOptions, pluginsBeforeSignIn, plugin
|
|
|
49
49
|
import { runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
|
|
50
50
|
import { checkSourceNames, sourceOptions, sourcesFor, sourcesVariable } from "./sources/source-settings.js";
|
|
51
51
|
import { renderVariant, thumbHash } from "./files/images.js";
|
|
52
|
+
import { pngIcons } from "./web-app/app-icons.js";
|
|
52
53
|
import { createAdmin } from "./create-admin.js";
|
|
53
54
|
import { handleDroppedConnections } from "./dropped-connections.js";
|
|
54
55
|
import { configExports } from "./resource-source.js";
|
|
@@ -74,4 +75,4 @@ import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/p
|
|
|
74
75
|
import { defineEmail } from "./mail/define-email.js";
|
|
75
76
|
import { startJobs } from "./jobs/runtime.js";
|
|
76
77
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
77
|
-
export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
78
|
+
export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/meta.js
CHANGED
|
@@ -28,7 +28,7 @@ const modelsHash = (deps) => sha256Hex(stableJson(deps.registry.entries.map((ent
|
|
|
28
28
|
* they could use once signed in, which keep their place in the bottom bar and the user menu.
|
|
29
29
|
* The ETag hashes all of it, so it differs per user and per roles.
|
|
30
30
|
*/
|
|
31
|
-
const createMeta = (deps, appName) => {
|
|
31
|
+
const createMeta = (deps, appName, appIcon) => {
|
|
32
32
|
const base = modelsHash(deps);
|
|
33
33
|
const forCaller = async (session) => {
|
|
34
34
|
const accesses = await Promise.all(deps.registry.entries.map((entry) => requestAccess(deps, entry, session)));
|
|
@@ -62,10 +62,11 @@ const createMeta = (deps, appName) => {
|
|
|
62
62
|
const sources = sourcesFor(deps.sources, session.user.roles);
|
|
63
63
|
const body = {
|
|
64
64
|
appName,
|
|
65
|
+
...appIcon && { appIcon },
|
|
65
66
|
resources: visible.map((access) => access.readable),
|
|
66
67
|
views,
|
|
67
68
|
pages,
|
|
68
|
-
permissions: Object.fromEntries(visible.map(({ entry, resolved }) => [entry.name, {
|
|
69
|
+
permissions: Object.fromEntries(visible.map(({ entry, resolved, full }) => [entry.name, {
|
|
69
70
|
read: resolved.operations.read || resolved.operations.list,
|
|
70
71
|
create: resolved.operations.create,
|
|
71
72
|
update: resolved.operations.update,
|
|
@@ -74,7 +75,7 @@ const createMeta = (deps, appName) => {
|
|
|
74
75
|
"create",
|
|
75
76
|
"update",
|
|
76
77
|
"delete"
|
|
77
|
-
].filter((op) => resolved.operations[op] && (resolved.recordChecks.includes(op) || resolved.rowFilter[op] !== void 0))
|
|
78
|
+
].filter((op) => resolved.operations[op] && (resolved.recordChecks.includes(op) || resolved.rowFilter[op] !== void 0 || (full.parents?.length ?? 0) > 0))
|
|
78
79
|
}])),
|
|
79
80
|
...signIn.length > 0 && { signIn },
|
|
80
81
|
...deps.userMenu && { userMenu: { items: deps.userMenu.items.filter(shown) } },
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { tenantScope } from "./tenant.js";
|
|
2
|
+
import { recordDecision, requestAccess } from "./request-access.js";
|
|
3
|
+
import { fetchRecord } from "./records.js";
|
|
4
|
+
import { fetchReadable } from "./fetch-readable.js";
|
|
5
|
+
//#region src/parent-access.ts
|
|
6
|
+
/** A record's identity across resources; `1` and `"1"` are the same record. */
|
|
7
|
+
const pathKey = (resource, key) => JSON.stringify([resource, ...key.map(String)]);
|
|
8
|
+
/**
|
|
9
|
+
* The parents these records name through `model.parents`, a null or missing relation naming none, sorted by path key:
|
|
10
|
+
* the order they are locked in, so two writes naming the same parents lock them in the same order. A parent named twice
|
|
11
|
+
* with the same need is checked once.
|
|
12
|
+
*/
|
|
13
|
+
const parentTargets = (model, records) => {
|
|
14
|
+
const targets = records.flatMap((record) => (model.parents ?? []).flatMap(({ field, access }) => {
|
|
15
|
+
const value = record[field];
|
|
16
|
+
if (value === void 0 || value === null) return [];
|
|
17
|
+
const resource = model.fields[field].relation.resource;
|
|
18
|
+
return [{
|
|
19
|
+
field,
|
|
20
|
+
access,
|
|
21
|
+
resource,
|
|
22
|
+
value,
|
|
23
|
+
key: pathKey(resource, [value])
|
|
24
|
+
}];
|
|
25
|
+
}));
|
|
26
|
+
return targets.filter((target, index) => targets.findIndex((other) => other.key === target.key && other.access === target.access) === index).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
27
|
+
};
|
|
28
|
+
const mayOn = async (scope, entry, key, need, path) => {
|
|
29
|
+
const access = await scope.accessOf(entry);
|
|
30
|
+
if (!access.resolved.operations.read) return false;
|
|
31
|
+
const found = await fetchReadable(scope.trx, access, key, "hide", scope.lock ? { lock: "no-key-update" } : {});
|
|
32
|
+
if (!found) return false;
|
|
33
|
+
if (need === "read") return true;
|
|
34
|
+
if (!access.resolved.operations.update) return false;
|
|
35
|
+
const tenant = tenantScope(entry, access.session)?.tenantValue;
|
|
36
|
+
const keyField = [access.full.fields[access.full.primaryKey[0]]];
|
|
37
|
+
const filter = access.resolved.rowFilter.update;
|
|
38
|
+
if (filter && !await fetchRecord(scope.trx, entry, key, tenant, {
|
|
39
|
+
filter,
|
|
40
|
+
fields: keyField
|
|
41
|
+
})) return false;
|
|
42
|
+
if (access.resolved.recordChecks.includes("update")) {
|
|
43
|
+
const decision = await recordDecision(access, "update", found.record, {});
|
|
44
|
+
if (decision === false) return false;
|
|
45
|
+
if (decision !== true && !await fetchRecord(scope.trx, entry, key, tenant, {
|
|
46
|
+
filter: decision,
|
|
47
|
+
fields: keyField
|
|
48
|
+
})) return false;
|
|
49
|
+
}
|
|
50
|
+
return parentsAllow(scope, access, [found.record], [...path, pathKey(entry.name, key)]);
|
|
51
|
+
};
|
|
52
|
+
const parentsAllow = async (scope, access, records, path) => {
|
|
53
|
+
for (const target of parentTargets(access.full, records)) {
|
|
54
|
+
if (path.includes(target.key)) return false;
|
|
55
|
+
const entry = scope.deps.registry.find(target.resource);
|
|
56
|
+
if (!entry) throw new Error(`Parent "${target.field}" of "${access.entry.name}" points at "${target.resource}", which the app does not have`);
|
|
57
|
+
if (!await mayOn(scope, entry, [target.value], target.access, path)) return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
const ownPath = (access, records) => records.filter((record) => access.full.primaryKey.every((name) => record[name] !== void 0 && record[name] !== null)).map((record) => pathKey(access.entry.name, access.full.primaryKey.map((name) => record[name])));
|
|
62
|
+
/**
|
|
63
|
+
* Whether the caller may write records of `access`'s resource given the parents they name (`.parent()`): an update passes
|
|
64
|
+
* the record before and after, so the parent it leaves and the one it moves to are locked in one order. With `lock`,
|
|
65
|
+
* each parent is locked FOR NO KEY UPDATE until the transaction ends, so a parent changed or deleted at the same moment
|
|
66
|
+
* cannot slip through; a read-only transaction cannot lock. Every call checks again; only each resource's access for
|
|
67
|
+
* the session is reused.
|
|
68
|
+
*/
|
|
69
|
+
const parentGuard = (trx, access, lock) => {
|
|
70
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
71
|
+
const accessOf = (entry) => {
|
|
72
|
+
if (!resolved.has(entry.name)) resolved.set(entry.name, requestAccess(access.deps, entry, access.session));
|
|
73
|
+
return resolved.get(entry.name);
|
|
74
|
+
};
|
|
75
|
+
const scope = {
|
|
76
|
+
trx,
|
|
77
|
+
deps: access.deps,
|
|
78
|
+
lock,
|
|
79
|
+
accessOf
|
|
80
|
+
};
|
|
81
|
+
return (...records) => parentsAllow(scope, access, records, ownPath(access, records));
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { parentGuard, parentTargets, pathKey };
|
package/dist/project-config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MobileConfig } from "./mobile-config.js";
|
|
2
|
+
import { AppIdentity } from "./app-identity.js";
|
|
2
3
|
import { AdminAuth } from "./better-auth/create-auth.js";
|
|
3
4
|
import { EventsOptions } from "./changes/events-options.js";
|
|
4
5
|
import { Authenticator } from "./types.js";
|
|
@@ -35,6 +36,8 @@ export type ProjectConfig = {
|
|
|
35
36
|
jobs?: Record<string, unknown>;
|
|
36
37
|
/** The app's iOS and Android build (see `MobileConfig`): `protobase mobile` needs it, and the API answers the app's origins. */
|
|
37
38
|
mobile?: MobileConfig;
|
|
39
|
+
/** The installed app's colours (see `AppIdentity`), which its web manifest carries. */
|
|
40
|
+
app?: AppIdentity;
|
|
38
41
|
options?: AdminOptions;
|
|
39
42
|
};
|
|
40
43
|
/** Typed identity, for `export default defineConfig({ extends: [...], config, auth, ... })` in `protobase.config.ts`. */
|
package/dist/project-config.js
CHANGED
|
@@ -143,14 +143,15 @@ const mergedOptions = (named) => {
|
|
|
143
143
|
*/
|
|
144
144
|
const mergeConfig = (...configs) => {
|
|
145
145
|
const named = configs.flatMap((config, index) => flatten(config, config.name ?? `config ${index + 1}`));
|
|
146
|
-
const { db, authenticate, auth, events, history, webhooks, mobile } = lastSet(named.map(({ config }) => ({
|
|
146
|
+
const { db, authenticate, auth, events, history, webhooks, mobile, app } = lastSet(named.map(({ config }) => ({
|
|
147
147
|
db: config.db,
|
|
148
148
|
authenticate: config.authenticate,
|
|
149
149
|
auth: config.auth,
|
|
150
150
|
events: config.events,
|
|
151
151
|
history: config.history,
|
|
152
152
|
webhooks: config.webhooks,
|
|
153
|
-
mobile: config.mobile
|
|
153
|
+
mobile: config.mobile,
|
|
154
|
+
app: config.app
|
|
154
155
|
})));
|
|
155
156
|
const module = mergedModule(named);
|
|
156
157
|
const functions = mergedFunctions(named);
|
|
@@ -171,6 +172,7 @@ const mergeConfig = (...configs) => {
|
|
|
171
172
|
...webhooks && { webhooks },
|
|
172
173
|
...jobs && { jobs },
|
|
173
174
|
...mobile && { mobile },
|
|
175
|
+
...app && { app },
|
|
174
176
|
...options && { options }
|
|
175
177
|
};
|
|
176
178
|
};
|
package/dist/read-service.js
CHANGED
|
@@ -1,40 +1,14 @@
|
|
|
1
1
|
import { notFound } from "./problem.js";
|
|
2
|
-
import {
|
|
3
|
-
import { recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields } from "./request-access.js";
|
|
2
|
+
import { requestAccess, requireOperation, visibleFields, writableFields } from "./request-access.js";
|
|
4
3
|
import { presentFiles } from "./files/present.js";
|
|
5
4
|
import { pickFields } from "./rows.js";
|
|
6
|
-
import {
|
|
5
|
+
import { fetchReadable } from "./fetch-readable.js";
|
|
7
6
|
import { rowPermissions } from "./row-permissions.js";
|
|
8
7
|
import { readTransaction } from "./transactions.js";
|
|
9
8
|
//#region src/read-service.ts
|
|
10
9
|
/** `edit` for fields the caller may change on this record, `read` for the rest (sensitive ones included); hidden fields are not listed at all. */
|
|
11
10
|
const fieldModes = (access, canUpdate) => Object.fromEntries(Object.keys(access.readable.fields).map((name) => [name, canUpdate && writableFields(access, "update").includes(name) ? "edit" : "read"]));
|
|
12
11
|
/**
|
|
13
|
-
* The row with this key if the caller may read it: inside their tenant and row filter, and passing a record-level read
|
|
14
|
-
* rule. Such a rule may look at fields the caller cannot see, so then the whole row is read; the caller cuts it down.
|
|
15
|
-
*/
|
|
16
|
-
const fetchReadable = async (trx, access, key, deleted, { fields, ...shape }) => {
|
|
17
|
-
const { entry } = access;
|
|
18
|
-
const tenant = tenantScope(entry, access.session)?.tenantValue;
|
|
19
|
-
const recordRule = access.resolved.recordChecks.includes("read");
|
|
20
|
-
const filter = targetFilter(access, "read");
|
|
21
|
-
const current = await fetchRecord(trx, entry, key, tenant, {
|
|
22
|
-
deleted,
|
|
23
|
-
...filter && { filter },
|
|
24
|
-
...shape,
|
|
25
|
-
...!recordRule && fields && { fields }
|
|
26
|
-
});
|
|
27
|
-
if (!current || !recordRule) return current;
|
|
28
|
-
const decision = await recordDecision(access, "read", current.record);
|
|
29
|
-
if (decision === false) return void 0;
|
|
30
|
-
if (decision !== true && !await fetchRecord(trx, entry, key, tenant, {
|
|
31
|
-
deleted,
|
|
32
|
-
filter: decision,
|
|
33
|
-
fields: [access.full.fields[access.full.primaryKey[0]]]
|
|
34
|
-
})) return void 0;
|
|
35
|
-
return current;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
12
|
* The record with this key as the caller may see it: readable fields only, inside the caller's tenant and row filter.
|
|
39
13
|
* A key outside the tenant, outside the row filter, or failing a record-level read rule is a plain 404.
|
|
40
14
|
*/
|
|
@@ -66,4 +40,4 @@ const readRecord = async (deps, entry, session, key, deleted = "hide") => {
|
|
|
66
40
|
};
|
|
67
41
|
};
|
|
68
42
|
//#endregion
|
|
69
|
-
export {
|
|
43
|
+
export { fieldModes, readRecord };
|
package/dist/records.js
CHANGED
|
@@ -12,6 +12,7 @@ const stored = async (entry, raw, shape = {}) => {
|
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
14
|
const rowsOf = async (db, query) => (await query.execute(db)).rows;
|
|
15
|
+
const lockClause = (lock) => lock === "no-key-update" ? sql` for no key update` : lock ? sql` for update` : sql``;
|
|
15
16
|
/** The row of the caller's tenant with this key (live unless `deleted` says otherwise), optionally narrowed by a filter. */
|
|
16
17
|
const fetchRecord = async (db, entry, key, tenant, options = {}) => {
|
|
17
18
|
const { filter, deleted, lock = false, ...shape } = options;
|
|
@@ -20,7 +21,7 @@ const fetchRecord = async (db, entry, key, tenant, options = {}) => {
|
|
|
20
21
|
...keyConditions(entry.model, key),
|
|
21
22
|
...filterCondition(entry.model, filter)
|
|
22
23
|
];
|
|
23
|
-
const query = sql`select ${returningList(entry, shape.fields)} from ${tableId(entry.model)} as t${whereClause(conditions)} limit 1${lock
|
|
24
|
+
const query = sql`select ${returningList(entry, shape.fields)} from ${tableId(entry.model)} as t${whereClause(conditions)} limit 1${lockClause(lock)}`;
|
|
24
25
|
return stored(entry, (await rowsOf(db, query))[0], shape);
|
|
25
26
|
};
|
|
26
27
|
const writeValue = (field, value) => field.type === "json" && value !== null ? JSON.stringify(value) : value;
|
package/dist/reveal-service.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { badRequest, notFound } from "./problem.js";
|
|
2
2
|
import { tenantScope } from "./tenant.js";
|
|
3
3
|
import { requestAccess, requireOperation } from "./request-access.js";
|
|
4
|
+
import { fetchReadable } from "./fetch-readable.js";
|
|
4
5
|
import { readTransaction } from "./transactions.js";
|
|
5
|
-
import { fetchReadable } from "./read-service.js";
|
|
6
6
|
import { encodeKey } from "@protobase/schema/internal";
|
|
7
7
|
//#region src/reveal-service.ts
|
|
8
8
|
/**
|
package/dist/row-permissions.js
CHANGED
|
@@ -2,6 +2,7 @@ import { tenantScope } from "./tenant.js";
|
|
|
2
2
|
import { recordDecision } from "./request-access.js";
|
|
3
3
|
import { aliasedColumn, columnId, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
4
4
|
import { fetchRecord } from "./records.js";
|
|
5
|
+
import { parentGuard } from "./parent-access.js";
|
|
5
6
|
import { sql } from "kysely";
|
|
6
7
|
import { compileFilter } from "@protobase/query";
|
|
7
8
|
//#region src/row-permissions.ts
|
|
@@ -21,8 +22,8 @@ const matching = async (trx, access, rows, filter) => {
|
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
24
|
* What the caller may do with each of these (exposed) rows: the operation must be allowed, the row must match the
|
|
24
|
-
* operation's row filter (checked in the database),
|
|
25
|
-
* informs the UI; every write is checked again by the write pipeline.
|
|
25
|
+
* operation's row filter (checked in the database), a record-level rule must pass for the stored record, and its parents
|
|
26
|
+
* (`.parent()`) must allow writing it. This only informs the UI; every write is checked again by the write pipeline.
|
|
26
27
|
*/
|
|
27
28
|
const rowPermissions = async (trx, access, rows) => {
|
|
28
29
|
const { resolved, entry, session } = access;
|
|
@@ -63,7 +64,23 @@ const rowPermissions = async (trx, access, rows) => {
|
|
|
63
64
|
if (decision !== true) result[i][operation] = (await matching(trx, access, [row], decision)).size > 0;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
await withParents(trx, access, rows, result);
|
|
66
68
|
return result;
|
|
67
69
|
};
|
|
70
|
+
const withParents = async (trx, access, rows, result) => {
|
|
71
|
+
const { full, entry, session } = access;
|
|
72
|
+
if (!full.parents?.length) return;
|
|
73
|
+
const guard = parentGuard(trx, access, false);
|
|
74
|
+
const fields = [.../* @__PURE__ */ new Set([...full.primaryKey, ...full.parents.map((parent) => parent.field)])].map((name) => full.fields[name]);
|
|
75
|
+
const tenant = tenantScope(entry, session)?.tenantValue;
|
|
76
|
+
for (const [i, row] of rows.entries()) {
|
|
77
|
+
const permissions = result[i];
|
|
78
|
+
if (!permissions.update && !permissions.delete) continue;
|
|
79
|
+
const stored = await fetchRecord(trx, entry, full.primaryKey.map((name) => row[name]), tenant, { fields });
|
|
80
|
+
const allowed = stored !== void 0 && await guard(stored.record);
|
|
81
|
+
permissions.update &&= allowed;
|
|
82
|
+
permissions.delete &&= allowed;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
68
85
|
//#endregion
|
|
69
86
|
export { rowPermissions };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/web-app/app-icons.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The PNG icons a project may put beside its `protobase.favicon.svg`, for platforms that take no SVG, each square at
|
|
4
|
+
* `size` pixels and answered on the app's own origin at `/<file>`: Android's 192 and 512 pixel icons, a 512 pixel
|
|
5
|
+
* maskable one whose mark keeps to the middle 80%, and the 180 pixel Apple touch icon an iPhone home screen shows.
|
|
6
|
+
*/
|
|
7
|
+
export declare const pngIcons: {
|
|
8
|
+
readonly icon192: {
|
|
9
|
+
readonly file: "protobase.icon-192.png";
|
|
10
|
+
readonly size: 192;
|
|
11
|
+
};
|
|
12
|
+
readonly icon512: {
|
|
13
|
+
readonly file: "protobase.icon-512.png";
|
|
14
|
+
readonly size: 512;
|
|
15
|
+
};
|
|
16
|
+
readonly maskable: {
|
|
17
|
+
readonly file: "protobase.icon-maskable.png";
|
|
18
|
+
readonly size: 512;
|
|
19
|
+
};
|
|
20
|
+
readonly appleTouch: {
|
|
21
|
+
readonly file: "protobase.apple-touch-icon.png";
|
|
22
|
+
readonly size: 180;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export type PngIcon = keyof typeof pngIcons;
|
|
26
|
+
/** The icons a project has of its own: `favicon` when the favicon is its own rather than Protobase's, and its PNGs. */
|
|
27
|
+
export type AppIcons = {
|
|
28
|
+
favicon: boolean;
|
|
29
|
+
png: PngIcon[];
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/web-app/app-icons.ts
|
|
2
|
+
/**
|
|
3
|
+
* The PNG icons a project may put beside its `protobase.favicon.svg`, for platforms that take no SVG, each square at
|
|
4
|
+
* `size` pixels and answered on the app's own origin at `/<file>`: Android's 192 and 512 pixel icons, a 512 pixel
|
|
5
|
+
* maskable one whose mark keeps to the middle 80%, and the 180 pixel Apple touch icon an iPhone home screen shows.
|
|
6
|
+
*/
|
|
7
|
+
const pngIcons = {
|
|
8
|
+
icon192: {
|
|
9
|
+
file: "protobase.icon-192.png",
|
|
10
|
+
size: 192
|
|
11
|
+
},
|
|
12
|
+
icon512: {
|
|
13
|
+
file: "protobase.icon-512.png",
|
|
14
|
+
size: 512
|
|
15
|
+
},
|
|
16
|
+
maskable: {
|
|
17
|
+
file: "protobase.icon-maskable.png",
|
|
18
|
+
size: 512
|
|
19
|
+
},
|
|
20
|
+
appleTouch: {
|
|
21
|
+
file: "protobase.apple-touch-icon.png",
|
|
22
|
+
size: 180
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { pngIcons };
|
|
@@ -4,9 +4,9 @@ import { Hono } from "hono";
|
|
|
4
4
|
/** The path under the system path (`/api`) the admin app's page links its manifest at. */
|
|
5
5
|
const webManifestPath = "/manifest.webmanifest";
|
|
6
6
|
/** Answers the web app manifest to anyone: the browser fetches it without the user's token, before they sign in. */
|
|
7
|
-
const webManifestRoute = (appName) => {
|
|
7
|
+
const webManifestRoute = (appName, identity, icons) => {
|
|
8
8
|
const app = new Hono();
|
|
9
|
-
const body = JSON.stringify(webManifest(appName));
|
|
9
|
+
const body = JSON.stringify(webManifest(appName, identity, icons));
|
|
10
10
|
app.get(webManifestPath, () => new Response(body, { headers: {
|
|
11
11
|
"content-type": "application/manifest+json",
|
|
12
12
|
"cache-control": "no-cache"
|
|
@@ -1,22 +1,41 @@
|
|
|
1
|
+
import { pngIcons } from "./app-icons.js";
|
|
1
2
|
//#region src/web-app/web-manifest.ts
|
|
2
3
|
/** Where the app's favicon is on its own origin, which `protobase build` and `protobase dev` always answer. */
|
|
3
4
|
const faviconPath = "/protobase.favicon.svg";
|
|
5
|
+
const manifestIcons = {
|
|
6
|
+
icon192: "any",
|
|
7
|
+
icon512: "any",
|
|
8
|
+
maskable: "maskable"
|
|
9
|
+
};
|
|
10
|
+
const isManifestIcon = (icon) => icon in manifestIcons;
|
|
11
|
+
const manifestIcon = (icon) => {
|
|
12
|
+
const { file, size } = pngIcons[icon];
|
|
13
|
+
return {
|
|
14
|
+
src: `/${file}`,
|
|
15
|
+
sizes: `${size}x${size}`,
|
|
16
|
+
type: "image/png",
|
|
17
|
+
...manifestIcons[icon] === "maskable" && { purpose: "maskable" }
|
|
18
|
+
};
|
|
19
|
+
};
|
|
4
20
|
/**
|
|
5
21
|
* The web app manifest that makes the admin app installable: named for the app, opening at its home page in a window
|
|
6
|
-
* of its own, with the favicon as its icon
|
|
22
|
+
* of its own, with the favicon as its icon, the project's PNGs beside it and its colours when it has them. An SVG with
|
|
23
|
+
* `sizes: "any"` meets Chrome's 192 and 512 pixel icon sizes; Safari and some Android launchers take only PNGs.
|
|
7
24
|
*/
|
|
8
|
-
const webManifest = (appName) => ({
|
|
25
|
+
const webManifest = (appName, { themeColor, backgroundColor } = {}, icons) => ({
|
|
9
26
|
name: appName,
|
|
10
27
|
short_name: appName,
|
|
11
28
|
id: "/",
|
|
12
29
|
start_url: "/",
|
|
13
30
|
scope: "/",
|
|
14
31
|
display: "standalone",
|
|
32
|
+
...themeColor && { theme_color: themeColor },
|
|
33
|
+
...backgroundColor && { background_color: backgroundColor },
|
|
15
34
|
icons: [{
|
|
16
35
|
src: faviconPath,
|
|
17
36
|
sizes: "any",
|
|
18
37
|
type: "image/svg+xml"
|
|
19
|
-
}]
|
|
38
|
+
}, ...(icons?.png ?? []).filter(isManifestIcon).map(manifestIcon)]
|
|
20
39
|
});
|
|
21
40
|
//#endregion
|
|
22
41
|
export { faviconPath, webManifest };
|
package/dist/write-pipeline.js
CHANGED
|
@@ -8,7 +8,7 @@ import { isDatabaseError } from "./error-handler.js";
|
|
|
8
8
|
import { attachFiles } from "./files/attach.js";
|
|
9
9
|
import { actorSettings, wakeDispatch } from "./changes/actor.js";
|
|
10
10
|
import { validateBody } from "./validate-body.js";
|
|
11
|
-
import { assertWritten, checkPrecondition, decide, denied, firstKeyField, keyOf, missing, notifier, shown } from "./write-shared.js";
|
|
11
|
+
import { assertWritten, checkParents, checkPrecondition, decide, denied, firstKeyField, keyOf, missing, notifier, shown } from "./write-shared.js";
|
|
12
12
|
import { applyUpdate, planUpdate } from "./write-update.js";
|
|
13
13
|
import { checkRankAfter } from "./rank-after.js";
|
|
14
14
|
//#region src/write-pipeline.ts
|
|
@@ -31,8 +31,11 @@ const create = async (trx, deps, access, body) => {
|
|
|
31
31
|
...tenantValues
|
|
32
32
|
};
|
|
33
33
|
const filter = await decide(access, "create", void 0, row);
|
|
34
|
+
const parents = checkParents(trx, access, "create");
|
|
35
|
+
await parents(row);
|
|
34
36
|
const created = await insertRecord(trx, entry, row, { etagFields: Object.keys(access.model.fields) });
|
|
35
37
|
await assertWritten(trx, access, created, "create", access.resolved.rowFilter.create, filter);
|
|
38
|
+
await parents(created.record);
|
|
36
39
|
if (full.position) await checkRankAfter(trx, deps, access, full.position.field, [{
|
|
37
40
|
key: keyOf(access, created.record),
|
|
38
41
|
before: null
|
|
@@ -77,6 +80,7 @@ const removeOrRestore = async (trx, deps, access, request) => {
|
|
|
77
80
|
filter,
|
|
78
81
|
fields: [firstKeyField(access)]
|
|
79
82
|
})) throw denied(entry, operation);
|
|
83
|
+
await checkParents(trx, access, operation)(current.record);
|
|
80
84
|
const notify = notifier(deps, access);
|
|
81
85
|
if (undelete) {
|
|
82
86
|
const restored = await restoreRecord(trx, entry, request.key, scope?.tenantValue, shape);
|
package/dist/write-shared.js
CHANGED
|
@@ -5,6 +5,7 @@ import { presentFiles } from "./files/present.js";
|
|
|
5
5
|
import { ifMatchSatisfied } from "./etag.js";
|
|
6
6
|
import { pickFields } from "./rows.js";
|
|
7
7
|
import { fetchRecord } from "./records.js";
|
|
8
|
+
import { parentGuard } from "./parent-access.js";
|
|
8
9
|
import { scheduleRemovedFiles } from "./files/attach.js";
|
|
9
10
|
//#region src/write-shared.ts
|
|
10
11
|
const missing = (entry) => notFound(`No ${entry.name} with this key`);
|
|
@@ -22,6 +23,17 @@ const decide = async (access, operation, record, input) => {
|
|
|
22
23
|
if (decision === false) throw denied(access.entry, operation);
|
|
23
24
|
return decision === true ? void 0 : decision;
|
|
24
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Refuses a write whose records' parents (`.parent()`) do not give the caller the access it needs, with the same answer
|
|
28
|
+
* as a refused record-level rule; each parent stays locked until the write commits.
|
|
29
|
+
*/
|
|
30
|
+
const checkParents = (trx, access, operation) => {
|
|
31
|
+
if (!access.full.parents?.length) return async (..._records) => {};
|
|
32
|
+
const guard = parentGuard(trx, access, true);
|
|
33
|
+
return async (...records) => {
|
|
34
|
+
if (!await guard(...records)) throw denied(access.entry, operation);
|
|
35
|
+
};
|
|
36
|
+
};
|
|
25
37
|
const keyOf = (access, record) => access.full.primaryKey.map((name) => record[name]);
|
|
26
38
|
const firstKeyField = (access) => access.full.fields[access.full.primaryKey[0]];
|
|
27
39
|
const assertWritten = async (trx, access, written, operation, ...filters) => {
|
|
@@ -53,4 +65,4 @@ const notifier = (deps, access) => async (trx, kind, before, after) => {
|
|
|
53
65
|
await scheduleRemovedFiles(access, kind, before, after);
|
|
54
66
|
};
|
|
55
67
|
//#endregion
|
|
56
|
-
export { assertWritten, checkPrecondition, decide, denied, firstKeyField, keyOf, missing, notifier, shown };
|
|
68
|
+
export { assertWritten, checkParents, checkPrecondition, decide, denied, firstKeyField, keyOf, missing, notifier, shown };
|
package/dist/write-update.js
CHANGED
|
@@ -3,11 +3,12 @@ import { targetFilter } from "./request-access.js";
|
|
|
3
3
|
import { fetchRecord, updateRecord } from "./records.js";
|
|
4
4
|
import { attachFiles } from "./files/attach.js";
|
|
5
5
|
import { validateBody } from "./validate-body.js";
|
|
6
|
-
import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier } from "./write-shared.js";
|
|
6
|
+
import { assertWritten, checkParents, checkPrecondition, decide, denied, firstKeyField, missing, notifier } from "./write-shared.js";
|
|
7
7
|
//#region src/write-update.ts
|
|
8
8
|
/**
|
|
9
9
|
* Everything an update needs before it writes: the row (locked, inside the caller's tenant and row filters), the ETag
|
|
10
|
-
* precondition, validation against the fields the caller may write,
|
|
10
|
+
* precondition, validation against the fields the caller may write, the record-level rule with its filter, and the
|
|
11
|
+
* parents (`.parent()`) it leaves and moves to, checked in one call so they are locked in one order.
|
|
11
12
|
*/
|
|
12
13
|
const planUpdate = async (trx, access, key, body, ifMatch, required = true) => {
|
|
13
14
|
const { entry, full } = access;
|
|
@@ -27,10 +28,16 @@ const planUpdate = async (trx, access, key, body, ifMatch, required = true) => {
|
|
|
27
28
|
filter,
|
|
28
29
|
fields: [firstKeyField(access)]
|
|
29
30
|
})) throw denied(entry, "update");
|
|
31
|
+
const parents = checkParents(trx, access, "update");
|
|
32
|
+
if (Object.keys(values).length > 0) await parents(current.record, {
|
|
33
|
+
...current.record,
|
|
34
|
+
...values
|
|
35
|
+
});
|
|
30
36
|
return {
|
|
31
37
|
current,
|
|
32
38
|
values,
|
|
33
|
-
...filter && { filter }
|
|
39
|
+
...filter && { filter },
|
|
40
|
+
parents
|
|
34
41
|
};
|
|
35
42
|
};
|
|
36
43
|
/** Writes a planned update, checks the row as written against the filters, and runs the hooks. */
|
|
@@ -39,6 +46,7 @@ const applyUpdate = async (trx, deps, access, key, plan) => {
|
|
|
39
46
|
const tenant = tenantScope(entry, access.session)?.tenantValue;
|
|
40
47
|
const updated = await updateRecord(trx, entry, key, tenant, plan.values, { etagFields: Object.keys(access.model.fields) });
|
|
41
48
|
await assertWritten(trx, access, updated, "update", access.resolved.rowFilter.update, plan.filter);
|
|
49
|
+
await plan.parents(updated.record);
|
|
42
50
|
await notifier(deps, access)(trx, "update", plan.current.record, updated.record);
|
|
43
51
|
return updated;
|
|
44
52
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
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": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@better-auth/passkey": "^1.7.7",
|
|
34
34
|
"@better-auth/sso": "^1.7.7",
|
|
35
|
-
"@protobase/layout": "0.
|
|
36
|
-
"@protobase/query": "0.
|
|
37
|
-
"@protobase/schema": "0.
|
|
35
|
+
"@protobase/layout": "0.28.1",
|
|
36
|
+
"@protobase/query": "0.28.1",
|
|
37
|
+
"@protobase/schema": "0.28.1",
|
|
38
38
|
"@react-email/render": "^2.1.0",
|
|
39
39
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
40
40
|
"@sentry/core": "^11.7.0",
|