@protobase/server 0.10.0 → 0.11.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/admin-options.d.ts +6 -0
- package/dist/check-views.js +28 -5
- package/dist/create-admin.js +4 -1
- package/dist/deps.d.ts +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/meta.js +4 -1
- package/dist/plugins/plugin-settings.d.ts +27 -0
- package/dist/plugins/plugin-settings.js +31 -0
- package/dist/view-access.js +136 -37
- package/package.json +4 -4
package/dist/admin-options.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuditQueue, PipelineHook, ScanGuardMode } from "./types.js";
|
|
2
|
+
import { PluginOption } from "./plugins/plugin-settings.js";
|
|
2
3
|
import { CorsOrigins } from "./api-cors.js";
|
|
3
4
|
import { AssistantOptions } from "./assistant/assistant-settings.js";
|
|
4
5
|
import { RuntimeOptions } from "./runtime/runtime-settings.js";
|
|
@@ -39,5 +40,10 @@ export type AdminOptions = {
|
|
|
39
40
|
cors?: CorsOrigins;
|
|
40
41
|
/** The runtime updates endpoint behind the top bar's update button (see `RuntimeOptions`); `false` turns it off. */
|
|
41
42
|
runtime?: RuntimeOptions | false;
|
|
43
|
+
/**
|
|
44
|
+
* The plugins the admin app loads (see `PluginOption`), before the ones the platform lists in `PROTOBASE_PLUGINS`;
|
|
45
|
+
* `false` turns both off.
|
|
46
|
+
*/
|
|
47
|
+
plugins?: PluginOption[] | false;
|
|
42
48
|
};
|
|
43
49
|
//#endregion
|
package/dist/check-views.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { relatedErrors } from "./view-related.js";
|
|
2
|
+
import { addressErrors, addressPlaceholders, appHrefError, displayFields, fieldRefsIn, hrefPlaceholders } from "@protobase/schema";
|
|
2
3
|
//#region src/check-views.ts
|
|
4
|
+
const linkFields = (run) => run?.kind !== "link" ? [] : "to" in run ? addressPlaceholders(run.to) : hrefPlaceholders(run.href);
|
|
3
5
|
const sensitiveErrors = (view, model) => {
|
|
4
6
|
const sensitive = (name) => Boolean(model.fields[name]?.sensitive);
|
|
5
7
|
return [
|
|
@@ -10,11 +12,12 @@ const sensitiveErrors = (view, model) => {
|
|
|
10
12
|
...(view.list?.sort ?? []).map(([name]) => name),
|
|
11
13
|
...view.list?.search ?? []
|
|
12
14
|
]],
|
|
15
|
+
["a list display", (view.list?.displays ?? []).flatMap(displayFields)],
|
|
13
16
|
["a filter", view.filters.map((widget) => widget.field)],
|
|
14
17
|
["the chart", view.chart ? [view.chart.field] : []],
|
|
15
18
|
["the sidebar", view.layout.flatMap((item) => item.kind === "sidebar" ? item.fields : [])],
|
|
16
19
|
["the recent records", view.nav?.recent ? [view.nav.recent.status] : []],
|
|
17
|
-
["an action", view.actions.flatMap((action) => action.run?.kind === "update" ? Object.keys(action.run.values) : action.run
|
|
20
|
+
["an action", view.actions.flatMap((action) => action.run?.kind === "update" ? Object.keys(action.run.values) : linkFields(action.run))]
|
|
18
21
|
].flatMap(([place, names]) => [...new Set(names.filter(sensitive))].map((name) => `sensitive field "${name}" cannot be in ${place}`));
|
|
19
22
|
};
|
|
20
23
|
const valueLabelErrors = (view, model) => Object.entries(view.fields).flatMap(([name, hints]) => {
|
|
@@ -23,20 +26,40 @@ const valueLabelErrors = (view, model) => Object.entries(view.fields).flatMap(([
|
|
|
23
26
|
if (field?.type !== "enum") return [`value labels of "${name}" need an enum field`];
|
|
24
27
|
return Object.keys(hints.valueLabels).filter((value) => !field.enumValues?.includes(value)).map((value) => `value label "${value}" is not a value of "${name}"`);
|
|
25
28
|
});
|
|
29
|
+
const linkErrors = (view, model, models, pages) => view.actions.flatMap(({ name, run }) => {
|
|
30
|
+
if (run?.kind !== "link") return [];
|
|
31
|
+
if ("href" in run) {
|
|
32
|
+
const error = appHrefError(run.href);
|
|
33
|
+
return error ? [`action "${name}": ${error}`] : [];
|
|
34
|
+
}
|
|
35
|
+
return addressErrors(run.to, {
|
|
36
|
+
models,
|
|
37
|
+
pages,
|
|
38
|
+
source: model
|
|
39
|
+
}).map((error) => `action "${name}": ${error}`);
|
|
40
|
+
});
|
|
41
|
+
const unknownFieldErrors = (view, model) => {
|
|
42
|
+
const unknown = (names, what) => names.filter((name) => !Object.hasOwn(model.fields, name)).map((name) => `${what} names field "${name}", which ${model.name} does not have`);
|
|
43
|
+
return [...(view.list?.displays ?? []).flatMap((display) => unknown(displayFields(display), `display "${display.name}"`)), ...view.layout.flatMap((item) => item.kind === "custom" ? unknown(fieldRefsIn(item.props), `custom section ${item.component}`) : [])];
|
|
44
|
+
};
|
|
26
45
|
/**
|
|
27
|
-
* Fails at startup when a view uses a sensitive field where it would show empty,
|
|
28
|
-
*
|
|
46
|
+
* Fails at startup when a view uses a sensitive field where it would show empty, a display or custom section names a
|
|
47
|
+
* field the resource does not have, labels values its enum does not have,
|
|
48
|
+
* has a related section that does not fit the resources, or a link to an address the app does not have.
|
|
29
49
|
*/
|
|
30
|
-
const checkViews = (registry, views) => {
|
|
50
|
+
const checkViews = (registry, views, pages = []) => {
|
|
31
51
|
const models = Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model]));
|
|
52
|
+
const pageNames = new Set(pages);
|
|
32
53
|
const problems = views.flatMap((view) => {
|
|
33
54
|
const model = models[view.resource];
|
|
34
55
|
if (!model) return [];
|
|
35
56
|
const related = view.layout.flatMap((item) => item.kind === "related" ? relatedErrors(item, view.resource, models).map((error) => `related "${item.title}": ${error}`) : []);
|
|
36
57
|
return [
|
|
37
58
|
...sensitiveErrors(view, model),
|
|
59
|
+
...unknownFieldErrors(view, model),
|
|
38
60
|
...valueLabelErrors(view, model),
|
|
39
|
-
...related
|
|
61
|
+
...related,
|
|
62
|
+
...linkErrors(view, model, models, pageNames)
|
|
40
63
|
].map((problem) => `View "${view.resource}": ${problem}`);
|
|
41
64
|
});
|
|
42
65
|
if (problems.length > 0) throw new Error(`The views do not match the resources:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
package/dist/create-admin.js
CHANGED
|
@@ -19,6 +19,7 @@ import { checkViews } from "./check-views.js";
|
|
|
19
19
|
import { createFilesRuntime } from "./files/runtime.js";
|
|
20
20
|
import { fileRoutes } from "./files/routes.js";
|
|
21
21
|
import { functionRoutes } from "./functions/routes.js";
|
|
22
|
+
import { pluginOptions } from "./plugins/plugin-settings.js";
|
|
22
23
|
import { runtimeUrl } from "./runtime/runtime-settings.js";
|
|
23
24
|
import { createMeta } from "./meta.js";
|
|
24
25
|
import { buildRegistry } from "./registry.js";
|
|
@@ -50,7 +51,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
50
51
|
checkShell(registry, viewModels, userMenuModel, pageModels.map((entry) => entry.name));
|
|
51
52
|
const bottomBarModels = bottomBars.map((bar) => bar.toBottomBarModel());
|
|
52
53
|
checkBottomBars(registry, bottomBarModels, pageModels.map((entry) => entry.name), declaredRoles(auth, options.roles));
|
|
53
|
-
checkViews(registry, viewModels);
|
|
54
|
+
checkViews(registry, viewModels, pageModels.map((entry) => entry.name));
|
|
54
55
|
checkPages(pageModels, {
|
|
55
56
|
models: Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model])),
|
|
56
57
|
actions: (resource) => new Set(viewModels.filter((view) => view.resource === resource).flatMap((view) => view.actions.map((action) => action.name)))
|
|
@@ -58,6 +59,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
58
59
|
const systemPath = basePath.slice(0, basePath.lastIndexOf("/"));
|
|
59
60
|
const assistant = assistantSettings(options.assistant, globalThis.process?.env ?? {});
|
|
60
61
|
const runtime = runtimeUrl(options.runtime, globalThis.process?.env ?? {});
|
|
62
|
+
const plugins = pluginOptions(options.plugins, globalThis.process?.env ?? {});
|
|
61
63
|
const files = createFilesRuntime({
|
|
62
64
|
...filesOptions && { options: filesOptions },
|
|
63
65
|
env: globalThis.process?.env ?? {},
|
|
@@ -85,6 +87,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
85
87
|
rowPermissions: (resource) => typeof options.rowPermissions === "object" ? !options.rowPermissions.except.includes(resource) : options.rowPermissions !== false,
|
|
86
88
|
...assistant && { assistant: assistant.kind === "external" ? assistant.url : `${systemPath}/assistant` },
|
|
87
89
|
...runtime && { runtime },
|
|
90
|
+
plugins,
|
|
88
91
|
...files && { files },
|
|
89
92
|
...registry.entries.some((entry) => entry.model.events) && { actorSetting: `${changesSchema(events)}.actor` }
|
|
90
93
|
};
|
package/dist/deps.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { FileProvider, FileStore, StoredObject } from "./files/store.js";
|
|
|
16
16
|
import { FilesOptions } from "./files/options.js";
|
|
17
17
|
import { CleanupEntry, CleanupSchedule, ScheduledDelete, storageCleanupSchedule } from "./files/cleanup-schedule.js";
|
|
18
18
|
import { CleanupReport } from "./files/cleanup-runner.js";
|
|
19
|
+
import { PluginOption, PluginRef, pluginOptions, pluginsFor, pluginsVariable } from "./plugins/plugin-settings.js";
|
|
19
20
|
import { ResourceSource, configExports } from "./resource-source.js";
|
|
20
21
|
import { RecordKeyInput, ToolRecord, ToolRecords } from "./assistant/records.js";
|
|
21
22
|
import { FunctionRecords, ListParams, RecordPage } from "./functions/records.js";
|
|
@@ -68,4 +69,4 @@ import { FatalJobError } from "./jobs/run-job.js";
|
|
|
68
69
|
import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
|
|
69
70
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
70
71
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
71
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, 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 ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, 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 TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints };
|
|
72
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, 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 ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, 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 TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
|
|
|
31
31
|
import { subscription } from "./webhooks/subscription.js";
|
|
32
32
|
import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
|
|
33
33
|
import { localFileStore, localFiles } from "./files/local-files.js";
|
|
34
|
+
import { pluginOptions, pluginsFor, pluginsVariable } from "./plugins/plugin-settings.js";
|
|
34
35
|
import { runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
|
|
35
36
|
import { createAdmin } from "./create-admin.js";
|
|
36
37
|
import { configExports } from "./resource-source.js";
|
|
@@ -51,4 +52,4 @@ import { createFileCleanup } from "./files/file-cleanup.js";
|
|
|
51
52
|
import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
|
|
52
53
|
import { startJobs } from "./jobs/runtime.js";
|
|
53
54
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
54
|
-
export { FatalJobError, HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints };
|
|
55
|
+
export { FatalJobError, HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints };
|
package/dist/meta.js
CHANGED
|
@@ -2,6 +2,7 @@ import { seesAssistant } from "./assistant/assistant-settings.js";
|
|
|
2
2
|
import { sha256Hex, stableJson } from "./hash.js";
|
|
3
3
|
import { requestAccess } from "./request-access.js";
|
|
4
4
|
import { activeRole } from "./active-role.js";
|
|
5
|
+
import { pluginsFor } from "./plugins/plugin-settings.js";
|
|
5
6
|
import { seesRuntime } from "./runtime/runtime-settings.js";
|
|
6
7
|
import { pruneView } from "./view-access.js";
|
|
7
8
|
import { prunePage } from "@protobase/layout";
|
|
@@ -35,6 +36,7 @@ const createMeta = (deps) => {
|
|
|
35
36
|
const shown = (item) => item.kind === "link" || (item.kind === "resource" ? visible.some((access) => access.entry.name === item.resource) : pages.some((page) => page.name === item.page));
|
|
36
37
|
const bar = bottomBarFor(deps.bottomBars, activeRole(session));
|
|
37
38
|
const barItems = bar?.items.filter(shown) ?? [];
|
|
39
|
+
const plugins = pluginsFor(deps.plugins, session.user.roles);
|
|
38
40
|
const body = {
|
|
39
41
|
resources: visible.map((access) => access.readable),
|
|
40
42
|
views,
|
|
@@ -56,7 +58,8 @@ const createMeta = (deps) => {
|
|
|
56
58
|
items: barItems
|
|
57
59
|
} },
|
|
58
60
|
...deps.assistant && seesAssistant(session.user.roles) && { assistant: { url: deps.assistant } },
|
|
59
|
-
...deps.runtime && seesRuntime(session.user.roles) && { runtime: { url: deps.runtime } }
|
|
61
|
+
...deps.runtime && seesRuntime(session.user.roles) && { runtime: { url: deps.runtime } },
|
|
62
|
+
...plugins.length > 0 && { plugins }
|
|
60
63
|
};
|
|
61
64
|
return {
|
|
62
65
|
body,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { JsonValue } from "@protobase/schema";
|
|
2
|
+
//#region src/plugins/plugin-settings.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A plugin the admin app loads at runtime: `url` names its plugin.json (`protobase build-plugin` writes it), on another
|
|
5
|
+
* origin or this one (`/plugins/...`). The plugin reads `settings` with `usePlugin()`. With `roles`, `/meta` names it
|
|
6
|
+
* only to callers holding one of them; without, to everyone.
|
|
7
|
+
*/
|
|
8
|
+
export type PluginOption = {
|
|
9
|
+
url: string;
|
|
10
|
+
settings?: Record<string, JsonValue>;
|
|
11
|
+
roles?: string[];
|
|
12
|
+
};
|
|
13
|
+
/** A plugin as `/meta` names it to a caller. */
|
|
14
|
+
export type PluginRef = {
|
|
15
|
+
url: string;
|
|
16
|
+
settings?: Record<string, JsonValue>;
|
|
17
|
+
};
|
|
18
|
+
/** The platform's plugins, as plugin.json URLs separated by commas, loaded after the ones `options.plugins` lists. */
|
|
19
|
+
export declare const pluginsVariable = "PROTOBASE_PLUGINS";
|
|
20
|
+
/**
|
|
21
|
+
* The plugins the app loads: those `options.plugins` lists, then those `PROTOBASE_PLUGINS` adds, each URL once. `false`
|
|
22
|
+
* turns both off. A value that is no URL stops the server at startup.
|
|
23
|
+
*/
|
|
24
|
+
export declare const pluginOptions: (option: PluginOption[] | false | undefined, env: Record<string, string | undefined>) => PluginOption[];
|
|
25
|
+
/** The plugins `/meta` names to a caller with `roles`: those for everyone, and those for one of their roles. */
|
|
26
|
+
export declare const pluginsFor: (plugins: readonly PluginOption[], roles: readonly string[]) => PluginRef[];
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/plugins/plugin-settings.ts
|
|
2
|
+
/** The platform's plugins, as plugin.json URLs separated by commas, loaded after the ones `options.plugins` lists. */
|
|
3
|
+
const pluginsVariable = "PROTOBASE_PLUGINS";
|
|
4
|
+
const checkedPluginUrl = (url, source) => {
|
|
5
|
+
if (url.startsWith("/") && !url.startsWith("//")) return url;
|
|
6
|
+
if (!URL.canParse(url)) throw new Error(`${source} names "${url}", which is no URL; use https://... or a path such as /plugins/name/plugin.json`);
|
|
7
|
+
const { protocol } = new URL(url);
|
|
8
|
+
if (protocol !== "https:" && protocol !== "http:") throw new Error(`${source} names "${url}", which must start with https:// or http://, not ${protocol}//`);
|
|
9
|
+
return url;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The plugins the app loads: those `options.plugins` lists, then those `PROTOBASE_PLUGINS` adds, each URL once. `false`
|
|
13
|
+
* turns both off. A value that is no URL stops the server at startup.
|
|
14
|
+
*/
|
|
15
|
+
const pluginOptions = (option, env) => {
|
|
16
|
+
if (option === false) return [];
|
|
17
|
+
const given = (option ?? []).map((plugin) => ({
|
|
18
|
+
...plugin,
|
|
19
|
+
url: checkedPluginUrl(plugin.url.trim(), "options.plugins")
|
|
20
|
+
}));
|
|
21
|
+
const listed = (env["PROTOBASE_PLUGINS"] ?? "").split(",").map((url) => url.trim()).filter((url) => url !== "").map((url) => ({ url: checkedPluginUrl(url, pluginsVariable) }));
|
|
22
|
+
const all = [...given, ...listed];
|
|
23
|
+
return all.filter((plugin, index) => all.findIndex((other) => other.url === plugin.url) === index);
|
|
24
|
+
};
|
|
25
|
+
/** The plugins `/meta` names to a caller with `roles`: those for everyone, and those for one of their roles. */
|
|
26
|
+
const pluginsFor = (plugins, roles) => plugins.filter((plugin) => !plugin.roles || plugin.roles.some((role) => roles.includes(role))).map(({ url, settings }) => ({
|
|
27
|
+
url,
|
|
28
|
+
...settings && { settings }
|
|
29
|
+
}));
|
|
30
|
+
//#endregion
|
|
31
|
+
export { pluginOptions, pluginsFor, pluginsVariable };
|
package/dist/view-access.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relatedErrors } from "./view-related.js";
|
|
2
2
|
import { resolveFieldPath } from "@protobase/layout";
|
|
3
|
-
import { navRecentErrors } from "@protobase/schema";
|
|
3
|
+
import { addressErrors, hrefPlaceholders, navRecentErrors, pruneByShape, shapes, tableDisplay } from "@protobase/schema";
|
|
4
4
|
//#region src/view-access.ts
|
|
5
5
|
const pruneRelated = (item, resource, models) => {
|
|
6
6
|
const { columns, ...rest } = item;
|
|
@@ -9,46 +9,145 @@ const pruneRelated = (item, resource, models) => {
|
|
|
9
9
|
...rest,
|
|
10
10
|
columns: visible
|
|
11
11
|
} : rest;
|
|
12
|
-
return relatedErrors(narrowed, resource, models).length === 0 ?
|
|
12
|
+
return relatedErrors(narrowed, resource, models).length === 0 ? narrowed : void 0;
|
|
13
13
|
};
|
|
14
|
+
const keepDefault = ({ default: opens, displays, ...list }) => {
|
|
15
|
+
const shown = displays && displays.length > 0 ? displays : void 0;
|
|
16
|
+
const kept = opens === tableDisplay || shown?.some((display) => display.name === opens);
|
|
17
|
+
return {
|
|
18
|
+
...list,
|
|
19
|
+
...shown && { displays: shown },
|
|
20
|
+
...opens !== void 0 && kept && { default: opens }
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const listShape = shapes.object({
|
|
24
|
+
columns: shapes.list(shapes.field),
|
|
25
|
+
sort: shapes.list(shapes.tuple(shapes.field, shapes.data)),
|
|
26
|
+
search: shapes.list(shapes.field),
|
|
27
|
+
displays: shapes.list(shapes.oneOf({
|
|
28
|
+
cards: shapes.object({
|
|
29
|
+
kind: shapes.data,
|
|
30
|
+
name: shapes.data,
|
|
31
|
+
label: shapes.data,
|
|
32
|
+
icon: shapes.data,
|
|
33
|
+
title: shapes.field,
|
|
34
|
+
subtitle: shapes.field,
|
|
35
|
+
badge: shapes.field,
|
|
36
|
+
meta: shapes.list(shapes.field)
|
|
37
|
+
}),
|
|
38
|
+
custom: shapes.object({
|
|
39
|
+
kind: shapes.data,
|
|
40
|
+
name: shapes.data,
|
|
41
|
+
label: shapes.data,
|
|
42
|
+
icon: shapes.data,
|
|
43
|
+
component: shapes.data,
|
|
44
|
+
fields: shapes.list(shapes.field),
|
|
45
|
+
props: shapes.refs
|
|
46
|
+
}, { requires: ["props"] })
|
|
47
|
+
})),
|
|
48
|
+
default: shapes.data
|
|
49
|
+
}, { after: keepDefault });
|
|
50
|
+
const widgetShape = shapes.oneOf({
|
|
51
|
+
range: shapes.object({
|
|
52
|
+
kind: shapes.data,
|
|
53
|
+
field: shapes.field,
|
|
54
|
+
histogram: shapes.data
|
|
55
|
+
}, { requires: ["field"] }),
|
|
56
|
+
facets: shapes.object({
|
|
57
|
+
kind: shapes.data,
|
|
58
|
+
field: shapes.field,
|
|
59
|
+
search: shapes.data
|
|
60
|
+
}, { requires: ["field"] }),
|
|
61
|
+
dateRange: shapes.object({
|
|
62
|
+
kind: shapes.data,
|
|
63
|
+
field: shapes.field,
|
|
64
|
+
presets: shapes.data
|
|
65
|
+
}, { requires: ["field"] }),
|
|
66
|
+
toggle: shapes.object({
|
|
67
|
+
kind: shapes.data,
|
|
68
|
+
field: shapes.field
|
|
69
|
+
}, { requires: ["field"] })
|
|
70
|
+
});
|
|
71
|
+
const layoutShape = (resource, models) => shapes.oneOf({
|
|
72
|
+
section: shapes.object({
|
|
73
|
+
kind: shapes.data,
|
|
74
|
+
title: shapes.data,
|
|
75
|
+
help: shapes.data,
|
|
76
|
+
fields: shapes.list(shapes.field, { nonEmpty: true })
|
|
77
|
+
}, { requires: ["fields"] }),
|
|
78
|
+
sidebar: shapes.object({
|
|
79
|
+
kind: shapes.data,
|
|
80
|
+
fields: shapes.list(shapes.field, { nonEmpty: true })
|
|
81
|
+
}, { requires: ["fields"] }),
|
|
82
|
+
related: shapes.map((item) => pruneRelated(item, resource, models)),
|
|
83
|
+
custom: shapes.object({
|
|
84
|
+
kind: shapes.data,
|
|
85
|
+
component: shapes.data,
|
|
86
|
+
title: shapes.data,
|
|
87
|
+
sidebar: shapes.data,
|
|
88
|
+
props: shapes.refs
|
|
89
|
+
}, { requires: ["props"] })
|
|
90
|
+
});
|
|
91
|
+
const linkShape = (model, models) => shapes.map((run) => {
|
|
92
|
+
return ("to" in run ? addressErrors(run.to, {
|
|
93
|
+
models,
|
|
94
|
+
source: model
|
|
95
|
+
}).length === 0 : hrefPlaceholders(run.href).every((name) => Object.hasOwn(model.fields, name))) ? run : void 0;
|
|
96
|
+
});
|
|
97
|
+
const actionShape = (model, models) => shapes.object({
|
|
98
|
+
name: shapes.data,
|
|
99
|
+
label: shapes.data,
|
|
100
|
+
icon: shapes.data,
|
|
101
|
+
confirm: shapes.data,
|
|
102
|
+
bulk: shapes.data,
|
|
103
|
+
run: shapes.oneOf({
|
|
104
|
+
update: shapes.object({
|
|
105
|
+
kind: shapes.data,
|
|
106
|
+
values: shapes.keys({ all: true })
|
|
107
|
+
}, { requires: ["values"] }),
|
|
108
|
+
delete: shapes.data,
|
|
109
|
+
link: linkShape(model, models)
|
|
110
|
+
})
|
|
111
|
+
}, { requires: ["run"] });
|
|
112
|
+
const viewShape = (resource, model, models) => shapes.object({
|
|
113
|
+
resource: shapes.data,
|
|
114
|
+
roles: shapes.data,
|
|
115
|
+
title: shapes.field,
|
|
116
|
+
searchResult: shapes.object({
|
|
117
|
+
title: shapes.list(shapes.field, { nonEmpty: true }),
|
|
118
|
+
subtitle: shapes.field
|
|
119
|
+
}),
|
|
120
|
+
nav: shapes.object({
|
|
121
|
+
hidden: shapes.data,
|
|
122
|
+
group: shapes.data,
|
|
123
|
+
order: shapes.data,
|
|
124
|
+
recent: shapes.map((recent) => navRecentErrors(recent, model).length === 0 ? recent : void 0)
|
|
125
|
+
}),
|
|
126
|
+
names: shapes.data,
|
|
127
|
+
icon: shapes.data,
|
|
128
|
+
help: shapes.data,
|
|
129
|
+
fields: shapes.keys(),
|
|
130
|
+
list: listShape,
|
|
131
|
+
filters: shapes.list(widgetShape),
|
|
132
|
+
layout: shapes.list(layoutShape(resource, models)),
|
|
133
|
+
chart: shapes.object({
|
|
134
|
+
field: shapes.field,
|
|
135
|
+
range: shapes.data,
|
|
136
|
+
granularity: shapes.data
|
|
137
|
+
}, { requires: ["field"] }),
|
|
138
|
+
saveFeedback: shapes.data,
|
|
139
|
+
actions: shapes.list(actionShape(model, models))
|
|
140
|
+
});
|
|
14
141
|
/**
|
|
15
|
-
* The view with every reference to a field the caller cannot read removed: columns, sort, search
|
|
16
|
-
* filter widgets,
|
|
17
|
-
*
|
|
18
|
-
* `models`, the caller's view of every resource.
|
|
142
|
+
* The view with every reference to a field the caller cannot read removed: list columns, sort, search and displays,
|
|
143
|
+
* field hints, filter widgets, record sections and custom components' props, the chart, title and search result, the
|
|
144
|
+
* actions that set or read one, and the sidebar's `recent` group. Nothing in it may name a hidden field. Related sections
|
|
145
|
+
* and links are checked against `models`, the caller's view of every resource.
|
|
19
146
|
*/
|
|
20
147
|
const pruneView = (view, model, models) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const resultTitle = searchResult?.title?.filter(visible);
|
|
25
|
-
const subtitle = searchResult?.subtitle;
|
|
26
|
-
return {
|
|
27
|
-
...rest,
|
|
28
|
-
...searchResult && { searchResult: {
|
|
29
|
-
...resultTitle?.length && { title: resultTitle },
|
|
30
|
-
...subtitle !== void 0 && visible(subtitle) && { subtitle }
|
|
31
|
-
} },
|
|
32
|
-
...nav && { nav: recent && navRecentErrors(recent, model).length === 0 ? nav : placement },
|
|
33
|
-
...title !== void 0 && visible(title) && { title },
|
|
34
|
-
...chart && visible(chart.field) && { chart },
|
|
35
|
-
...list && { list: {
|
|
36
|
-
columns: list.columns.filter(visible),
|
|
37
|
-
...list.sort && { sort: list.sort.filter(([name]) => visible(name)) },
|
|
38
|
-
...list.search && { search: list.search.filter(visible) }
|
|
39
|
-
} },
|
|
40
|
-
fields: Object.fromEntries(Object.entries(view.fields).filter(([name]) => visible(name))),
|
|
41
|
-
filters: view.filters.filter((widget) => visible(widget.field)),
|
|
42
|
-
actions: view.actions.filter((action) => action.run?.kind !== "update" || Object.keys(action.run.values).every(visible)),
|
|
43
|
-
layout: view.layout.flatMap((item) => {
|
|
44
|
-
if (item.kind === "related") return pruneRelated(item, view.resource, models);
|
|
45
|
-
const fields = item.fields.filter(visible);
|
|
46
|
-
return fields.length > 0 ? [{
|
|
47
|
-
...item,
|
|
48
|
-
fields
|
|
49
|
-
}] : [];
|
|
50
|
-
})
|
|
51
|
-
};
|
|
148
|
+
const pruned = pruneByShape(view, viewShape(view.resource, model, models), (name) => Object.hasOwn(model.fields, name));
|
|
149
|
+
if (!pruned) throw new Error(`The view of ${view.resource} is not an object`);
|
|
150
|
+
return pruned;
|
|
52
151
|
};
|
|
53
152
|
//#endregion
|
|
54
153
|
export { pruneView };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@better-auth/passkey": "^1.7.7",
|
|
33
|
-
"@protobase/layout": "0.
|
|
34
|
-
"@protobase/query": "0.
|
|
35
|
-
"@protobase/schema": "0.
|
|
33
|
+
"@protobase/layout": "0.11.1",
|
|
34
|
+
"@protobase/query": "0.11.1",
|
|
35
|
+
"@protobase/schema": "0.11.1",
|
|
36
36
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
37
37
|
"@simplewebauthn/server": "^13.3.1",
|
|
38
38
|
"better-auth": "^1.7.7",
|