@protobase/server 0.21.2 → 0.21.4

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-name.js CHANGED
@@ -1,9 +1,11 @@
1
1
  //#region src/app-name.ts
2
2
  const appNameVariable = "PROTOBASE_APP_NAME";
3
+ /** The app's name when neither the app nor the platform names it. */
4
+ const defaultAppName = "Protobase";
3
5
  /**
4
6
  * The app's name the platform passes in `PROTOBASE_APP_NAME`, such as `Property Finder`; `undefined` when it is not
5
7
  * set. Protobase names the app with it in sign-in prompts and in the mail it sends, unless the app names itself.
6
8
  */
7
9
  const readAppName = (env) => env["PROTOBASE_APP_NAME"]?.trim() || void 0;
8
10
  //#endregion
9
- export { appNameVariable, readAppName };
11
+ export { appNameVariable, defaultAppName, readAppName };
@@ -800,7 +800,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
800
800
  expiresAt: Date;
801
801
  token: string;
802
802
  ipAddress?: string | null | undefined;
803
- userAgent?: string | null | undefined;
803
+ userAgent? /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */ : string | null | undefined;
804
804
  };
805
805
  };
806
806
  }>>[];
@@ -3749,15 +3749,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
3749
3749
  name?: string;
3750
3750
  slug?: string;
3751
3751
  logo?: string | null;
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>;
3752
+ metadata?: Record<string, any>;
3761
3753
  [key: string]: any;
3762
3754
  };
3763
3755
  }>;
@@ -4240,7 +4232,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
4240
4232
  hooks: {
4241
4233
  after: {
4242
4234
  matcher: (context: {
4243
- path?: string;
4235
+ path? /** Sign-in attempts allowed per minute and client, default 5; also the sign-up requests allowed. */ : string;
4244
4236
  }) => boolean;
4245
4237
  handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
4246
4238
  }[];
@@ -4374,6 +4366,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
4374
4366
  /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
4375
4367
  signUp: boolean;
4376
4368
  socialProviders: string[];
4369
+ /** The app's name it signs in and mails as: `appName`, else `PROTOBASE_APP_NAME`, else `Protobase`. */
4370
+ appName: string;
4377
4371
  operator: string | undefined;
4378
4372
  platformSignIn: {
4379
4373
  provider: string;
@@ -1,5 +1,5 @@
1
1
  import { multiHostBaseURL, readOrigins, resolveOrigins } from "./origins.js";
2
- import { readAppName } from "../app-name.js";
2
+ import { defaultAppName, readAppName } from "../app-name.js";
3
3
  import { platformMailer } from "../mail/platform-mailer.js";
4
4
  import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
5
5
  import { roleDefinitions } from "./organizations/role-definitions.js";
@@ -29,7 +29,6 @@ const platformSignInSettings = () => readPlatformSignIn(globalThis.process?.env
29
29
  const platformOrigins = () => readOrigins(globalThis.process?.env ?? {});
30
30
  const platformPasskeyDomain = () => readPasskeyDomain(globalThis.process?.env ?? {});
31
31
  const platformAppName = () => readAppName(globalThis.process?.env ?? {});
32
- const defaultAppName = "Protobase";
33
32
  const tokenLifetime = "15m";
34
33
  const authBasePath = "/api/auth";
35
34
  /**
@@ -277,6 +276,8 @@ const createAuth = (options) => {
277
276
  /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
278
277
  signUp: Boolean(signUpRole && mailer),
279
278
  socialProviders,
279
+ /** The app's name it signs in and mails as: `appName`, else `PROTOBASE_APP_NAME`, else `Protobase`. */
280
+ appName: appName ?? "Protobase",
280
281
  operator: operator?.name,
281
282
  platformSignIn: platformSignIn && {
282
283
  provider: platformSignIn.provider,
@@ -0,0 +1,38 @@
1
+ import { HttpProblem } from "./problem.js";
2
+ import { requestAccess } from "./request-access.js";
3
+ //#region src/blocked-delete.ts
4
+ /**
5
+ * The resource whose rows kept `deleted`'s record from being deleted, from the table a foreign key violation names, and
6
+ * its relation field pointing at `deleted` when there is exactly one, on a single column, so its list can be filtered by
7
+ * it. `undefined` when no resource or several read that table, or none of its fields points at `deleted`, as when a
8
+ * cascade was blocked further down.
9
+ */
10
+ const blockingRelation = (error, deleted, entries) => {
11
+ const tables = entries.filter(({ model }) => model.table.name === error.table && (model.table.schema === void 0 || model.table.schema === error.schema));
12
+ if (tables.length !== 1) return void 0;
13
+ const entry = tables[0];
14
+ const pointing = Object.entries(entry.model.fields).filter(([, field]) => field.type === "relation" && field.relation?.resource === deleted.name);
15
+ if (pointing.length === 0) return void 0;
16
+ const only = pointing.length === 1 ? pointing[0] : void 0;
17
+ return only && only[1].relation.columns.length === 1 ? {
18
+ entry,
19
+ field: only[0]
20
+ } : { entry };
21
+ };
22
+ /**
23
+ * The 409 for a delete other records still point at. It names their resource (`blockedBy`) only to a caller who may
24
+ * list it, and the field pointing at the record only when they may filter by it, as with any field they cannot see.
25
+ */
26
+ const blockedDelete = async (deps, access, error) => {
27
+ const match = blockingRelation(error, access.entry, deps.registry.entries);
28
+ const blocking = match && await requestAccess(deps, match.entry, access.session);
29
+ const listed = blocking && blocking.resolved.operations.list;
30
+ const field = listed && match.field && blocking.model.fields[match.field]?.filterable ? match.field : void 0;
31
+ const blockedBy = listed ? {
32
+ resource: match.entry.name,
33
+ ...field && { field }
34
+ } : void 0;
35
+ return new HttpProblem(409, "conflict", "Conflict", "Other records still point at this record", blockedBy && { blockedBy });
36
+ };
37
+ //#endregion
38
+ export { blockedDelete, blockingRelation };
@@ -6,6 +6,7 @@ import { builtInAssistant } from "./assistant/built-in-assistant.js";
6
6
  import { consoleAuditQueue } from "./audit/console-queue.js";
7
7
  import { webauthnPath } from "./better-auth/origins.js";
8
8
  import { apiCors, readCorsOrigins } from "./api-cors.js";
9
+ import { readAppName } from "./app-name.js";
9
10
  import { authBasePath } from "./better-auth/create-auth.js";
10
11
  import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
11
12
  import { statusRoute } from "./better-auth/status-route.js";
@@ -131,7 +132,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
131
132
  ...auth && { actorNames: betterAuthActorNames(auth) }
132
133
  } }
133
134
  };
134
- const meta = createMeta(deps);
135
+ const meta = createMeta(deps, auth?.appName ?? readAppName(globalThis.process?.env ?? {}) ?? "Protobase");
135
136
  const onError = createErrorHandler(options.onUnhandledError);
136
137
  const guarded = (routes, caller = authenticate) => {
137
138
  const app = new Hono();
@@ -41,4 +41,4 @@ const createErrorHandler = (onUnhandledError) => (error, c) => {
41
41
  return problemResponse(new HttpProblem(500, "internal-error", titles[500], "The server failed to handle the request"), c.req.path);
42
42
  };
43
43
  //#endregion
44
- export { createErrorHandler, problemOf };
44
+ export { createErrorHandler, isDatabaseError, problemOf };
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) => {
31
+ const createMeta = (deps, appName) => {
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)));
@@ -55,6 +55,7 @@ const createMeta = (deps) => {
55
55
  const plugins = pluginsFor(deps.plugins, session.user.roles);
56
56
  const sources = sourcesFor(deps.sources, session.user.roles);
57
57
  const body = {
58
+ appName,
58
59
  resources: visible.map((access) => access.readable),
59
60
  views,
60
61
  pages,
@@ -1,8 +1,10 @@
1
1
  import { HttpProblem, badRequest } from "./problem.js";
2
+ import { isDatabaseError } from "./error-handler.js";
2
3
  import { deleteRecord, fetchRecord, insertRecord, restoreRecord } from "./records.js";
3
4
  import { tenantScope } from "./tenant.js";
4
5
  import { requestAccess, requireOperation, targetFilter, writableFields } from "./request-access.js";
5
6
  import { writeTransaction } from "./transactions.js";
7
+ import { blockedDelete } from "./blocked-delete.js";
6
8
  import { attachFiles } from "./files/attach.js";
7
9
  import { actorSettings, wakeDispatch } from "./changes/actor.js";
8
10
  import { validateBody } from "./validate-body.js";
@@ -71,7 +73,10 @@ const removeOrRestore = async (trx, deps, access, request) => {
71
73
  await notify(trx, "undelete", current.record, restored.record);
72
74
  return shown(access, restored);
73
75
  }
74
- const removed = await deleteRecord(trx, entry, request.key, scope?.tenantValue, shape);
76
+ const removed = await deleteRecord(trx, entry, request.key, scope?.tenantValue, shape).catch(async (error) => {
77
+ if (isDatabaseError(error) && error.code === "23503") throw await blockedDelete(deps, access, error);
78
+ throw error;
79
+ });
75
80
  await notify(trx, "delete", current.record, void 0);
76
81
  return shown(access, removed);
77
82
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.21.2",
3
+ "version": "0.21.4",
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.21.2",
34
- "@protobase/query": "0.21.2",
35
- "@protobase/schema": "0.21.2",
33
+ "@protobase/layout": "0.21.4",
34
+ "@protobase/query": "0.21.4",
35
+ "@protobase/schema": "0.21.4",
36
36
  "@react-email/render": "^2.1.0",
37
37
  "@scalar/hono-api-reference": "^0.12.10",
38
38
  "@sentry/core": "^11.7.0",