@protobase/server 0.67.0 → 0.67.2
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/config-exports.d.ts +3 -1
- package/dist/config-exports.js +3 -1
- package/dist/create-admin.d.ts +4 -1
- package/dist/create-admin.js +18 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/package.js +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/user-tables/check-definition.d.ts +12 -0
- package/dist/user-tables/check-definition.js +42 -0
- package/dist/user-tables/definition-resource.d.ts +12 -0
- package/dist/user-tables/definition-resource.js +62 -0
- package/dist/user-tables/definitions-version.d.ts +10 -0
- package/dist/user-tables/definitions-version.js +16 -0
- package/dist/user-tables/latest-admin.js +47 -0
- package/dist/user-tables/names.d.ts +18 -0
- package/dist/user-tables/names.js +22 -0
- package/dist/user-tables/read-definitions.d.ts +22 -0
- package/dist/user-tables/read-definitions.js +33 -0
- package/dist/user-tables/reloading-admin.d.ts +40 -0
- package/dist/user-tables/reloading-admin.js +63 -0
- package/dist/user-tables/schema-sql.js +35 -0
- package/dist/user-tables/schema.d.ts +10 -0
- package/dist/user-tables/schema.js +34 -0
- package/dist/user-tables/served-user-tables.d.ts +9 -0
- package/dist/user-tables/served-user-tables.js +30 -0
- package/dist/user-tables/user-table-resources.d.ts +16 -0
- package/dist/user-tables/user-table-resources.js +32 -0
- package/package.json +4 -4
package/dist/config-exports.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { configExports } from "./resource-source.js";
|
|
2
|
+
import { definitionsSchema, userTablesSchema } from "./user-tables/names.js";
|
|
3
|
+
import { userTableResources } from "./user-tables/user-table-resources.js";
|
|
2
4
|
import { builtInResources } from "./built-in-resources.js";
|
|
3
5
|
import { webhooksModule } from "./webhooks/table/webhooks-module.js";
|
|
4
|
-
export { builtInResources, configExports, webhooksModule };
|
|
6
|
+
export { builtInResources, configExports, definitionsSchema, userTableResources, userTablesSchema, webhooksModule };
|
package/dist/config-exports.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { builtInResources } from "./built-in-resources.js";
|
|
2
2
|
import { configExports } from "./resource-source.js";
|
|
3
|
+
import { definitionsSchema, userTablesSchema } from "./user-tables/names.js";
|
|
4
|
+
import { userTableResources } from "./user-tables/user-table-resources.js";
|
|
3
5
|
import { webhooksModule } from "./webhooks/table/webhooks-module.js";
|
|
4
|
-
export { builtInResources, configExports, webhooksModule };
|
|
6
|
+
export { builtInResources, configExports, definitionsSchema, userTableResources, userTablesSchema, webhooksModule };
|
package/dist/create-admin.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { Difficulty } from "./forms/challenge.js";
|
|
|
17
17
|
import { ResourceSource } from "./resource-source.js";
|
|
18
18
|
import { FunctionSource } from "./functions/define-function.js";
|
|
19
19
|
import { AdminOptions } from "./admin-options.js";
|
|
20
|
+
import { UserTableSources } from "./user-tables/served-user-tables.js";
|
|
20
21
|
import { AdminAuth } from "./better-auth/create-auth.js";
|
|
21
22
|
import { BottomBarSource, UserMenuSource } from "@protobase/schema";
|
|
22
23
|
import { Db } from "@protobase/query";
|
|
@@ -26,6 +27,8 @@ import { UpgradeWebSocket } from "hono/ws";
|
|
|
26
27
|
export type CreateAdminInput = {
|
|
27
28
|
resources: ResourceSource[];
|
|
28
29
|
views?: ViewSource[];
|
|
30
|
+
/** The tables users add, from their definitions (`userTableResources`): one a resource here or a built-in names is left out with a warning. */
|
|
31
|
+
userTables?: UserTableSources;
|
|
29
32
|
/** The `userMenu(...)` export of the config module: pages under the signed-in user in the sidebar. */
|
|
30
33
|
userMenu?: UserMenuSource;
|
|
31
34
|
/** The `bottomBar(...)` exports of the config module, in order: tabs on phones for the roles each names. */
|
|
@@ -99,5 +102,5 @@ export type CreateAdminInput = {
|
|
|
99
102
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
100
103
|
};
|
|
101
104
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
102
|
-
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, secret, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, realtime, notifications, status, calls, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
105
|
+
export declare const createAdmin: ({ resources: appResources, views: appViews, userTables, pages, userMenu, bottomBars, db, authenticate, auth, functions, secret, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, realtime, notifications, status, calls, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
103
106
|
//#endregion
|
package/dist/create-admin.js
CHANGED
|
@@ -112,6 +112,7 @@ import { recordRoutes } from "./routes/record.js";
|
|
|
112
112
|
import { firstAdminUrl } from "./better-auth/first-admin-url.js";
|
|
113
113
|
import { createScanGuard } from "./scan-guard.js";
|
|
114
114
|
import { builtInResources } from "./built-in-resources.js";
|
|
115
|
+
import { servedUserTables } from "./user-tables/served-user-tables.js";
|
|
115
116
|
import { Hono } from "hono";
|
|
116
117
|
import { accountPage, accountPageName, checkPages, settingsResources, withSettingsFields } from "@protobase/layout";
|
|
117
118
|
import { guestCallsPath, ringsPath, statusPath } from "@protobase/schema/internal";
|
|
@@ -122,7 +123,7 @@ const declaredRoles = (auth, roles) => {
|
|
|
122
123
|
return names.length > 0 ? [...new Set(names)] : void 0;
|
|
123
124
|
};
|
|
124
125
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
125
|
-
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, secret, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, realtime, notifications, status, calls, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
126
|
+
const createAdmin = ({ resources: appResources, views: appViews = [], userTables, pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, secret, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, realtime, notifications, status, calls, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
126
127
|
const basePath = options.basePath ?? "/api/v1";
|
|
127
128
|
const appModels = appResources.map((resource) => resource.toModel());
|
|
128
129
|
const withComments = appModels.some((model) => model.comments);
|
|
@@ -136,8 +137,21 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
136
137
|
notifications: notifications !== void 0,
|
|
137
138
|
...events && { events }
|
|
138
139
|
});
|
|
139
|
-
const
|
|
140
|
-
|
|
140
|
+
const served = servedUserTables(userTables ?? {
|
|
141
|
+
resources: [],
|
|
142
|
+
views: []
|
|
143
|
+
}, [...appModels, ...builtIn.resources.map((resource) => resource.toModel())].map((model) => model.name));
|
|
144
|
+
for (const warning of served.warnings) console.warn(warning);
|
|
145
|
+
const resources = [
|
|
146
|
+
...appResources,
|
|
147
|
+
...builtIn.resources,
|
|
148
|
+
...served.resources
|
|
149
|
+
];
|
|
150
|
+
const views = [
|
|
151
|
+
...appViews,
|
|
152
|
+
...builtIn.views,
|
|
153
|
+
...served.views
|
|
154
|
+
];
|
|
141
155
|
const commentUsers = withComments ? givenCommentUsers ?? (auth && betterAuthCommentUsers(auth)) : void 0;
|
|
142
156
|
const mentions = withComments ? mentionJob(commentUsers, shownAppName(auth, globalThis.process?.env ?? {})) : void 0;
|
|
143
157
|
const registry = buildRegistry(resources);
|
|
@@ -551,6 +565,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
551
565
|
return Object.assign(app, {
|
|
552
566
|
startRealtimePrune: prune,
|
|
553
567
|
decryptField,
|
|
568
|
+
metaPath: `${deps.systemPath}/meta`,
|
|
554
569
|
jobs: [
|
|
555
570
|
...webhooks.jobs,
|
|
556
571
|
...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,10 @@ import { eventMatcher } from "./changes/match-events.js";
|
|
|
101
101
|
import { parseColumns, readOutbox } from "./changes/outbox.js";
|
|
102
102
|
import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
|
|
103
103
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
104
|
+
import { checkUserTablesSchema, userTablesMigration } from "./user-tables/schema.js";
|
|
105
|
+
import { UserTables, userTableResources } from "./user-tables/user-table-resources.js";
|
|
106
|
+
import { readDefinitionsVersion } from "./user-tables/definitions-version.js";
|
|
107
|
+
import { reloadingAdmin } from "./user-tables/reloading-admin.js";
|
|
104
108
|
import { checkRealtimeRelay, realtimeRelayMigration } from "./realtime/relay/relay-schema.js";
|
|
105
109
|
import { channelSendsMigration, checkChannelSends } from "./channels/sends-schema.js";
|
|
106
110
|
import { checkComments, commentsMigration } from "./comments/comments-sql.js";
|
|
@@ -184,4 +188,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
|
|
|
184
188
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
185
189
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
186
190
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
|
|
187
|
-
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailAttachment, type MailMessage, MailServerTimeoutError, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PdfComponent, type PdfOptions, type PdfTemplate, PersonalMailError, type PersonalMailErrorCode, type PersonalMailProviderId, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, 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 SendNotification, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StatusOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, onDoNotDisturb, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
191
|
+
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailAttachment, type MailMessage, MailServerTimeoutError, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PdfComponent, type PdfOptions, type PdfTemplate, PersonalMailError, type PersonalMailErrorCode, type PersonalMailProviderId, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, 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 SendNotification, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StatusOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type UserTables, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkUserTablesSchema, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, onDoNotDisturb, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readDefinitionsVersion, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, reloadingAdmin, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, userTableResources, userTablesMigration, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,10 @@ import { authStoreTables } from "./better-auth/auth-tables.js";
|
|
|
113
113
|
import { checkResourcesOutsideAuthStore } from "./better-auth/store/resources-outside.js";
|
|
114
114
|
import { checkComputed, computedMigration } from "./computed/computed-schema.js";
|
|
115
115
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
116
|
+
import { checkUserTablesSchema, userTablesMigration } from "./user-tables/schema.js";
|
|
117
|
+
import { userTableResources } from "./user-tables/user-table-resources.js";
|
|
118
|
+
import { readDefinitionsVersion } from "./user-tables/definitions-version.js";
|
|
119
|
+
import { reloadingAdmin } from "./user-tables/reloading-admin.js";
|
|
116
120
|
import { checkRealtimeRelay, realtimeRelayMigration } from "./realtime/relay/relay-schema.js";
|
|
117
121
|
import { channelSendsMigration, checkChannelSends } from "./channels/sends-schema.js";
|
|
118
122
|
import { callsMigration, checkCalls } from "./calls/calls-sql.js";
|
|
@@ -135,4 +139,4 @@ import { defineEmail } from "./mail/define-email.js";
|
|
|
135
139
|
import { definePdf } from "./pdf/define-pdf.js";
|
|
136
140
|
import { startJobs } from "./jobs/runtime.js";
|
|
137
141
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
138
|
-
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MailServerTimeoutError, MissingCredentials, ModelError, NotificationSendError, PersonalMailError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, onDoNotDisturb, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
142
|
+
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MailServerTimeoutError, MissingCredentials, ModelError, NotificationSendError, PersonalMailError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkUserTablesSchema, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, onDoNotDisturb, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readDefinitionsVersion, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, reloadingAdmin, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, userTableResources, userTablesMigration, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/package.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -147,11 +147,12 @@ export type PageSource = {
|
|
|
147
147
|
/**
|
|
148
148
|
* The admin API, the jobs it needs a worker to run (the delivery of resource events), the pruning of the realtime
|
|
149
149
|
* tables a serving process starts once it listens and stops as it closes, and the read of encrypted fields that
|
|
150
|
-
* `startJobs` gives the jobs' context.
|
|
150
|
+
* `startJobs` gives the jobs' context, and the path it answers `/meta` at.
|
|
151
151
|
*/
|
|
152
152
|
export type Admin = Hono<AdminEnv> & {
|
|
153
153
|
jobs: JobDefinition[];
|
|
154
154
|
startRealtimePrune: () => RealtimePrune;
|
|
155
155
|
decryptField: FieldDecrypter;
|
|
156
|
+
metaPath: string;
|
|
156
157
|
};
|
|
157
158
|
//#endregion
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DefinitionFieldType } from "./names.js";
|
|
2
|
+
import { StoredDefinition, StoredField } from "./read-definitions.js";
|
|
3
|
+
//#region src/user-tables/check-definition.d.ts
|
|
4
|
+
/** A field of a definition that passed `checkDefinition`: its name is a safe identifier and its type one of the eight. */
|
|
5
|
+
export type CheckedField = Omit<StoredField, 'type'> & {
|
|
6
|
+
type: DefinitionFieldType;
|
|
7
|
+
};
|
|
8
|
+
/** A definition that passed `checkDefinition`, which a resource is built from. */
|
|
9
|
+
export type CheckedDefinition = Omit<StoredDefinition, 'fields'> & {
|
|
10
|
+
fields: CheckedField[];
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { definitionColumnTypes, definitionFieldTypes, isUserTableName, userTablesSchema } from "./names.js";
|
|
2
|
+
//#region src/user-tables/check-definition.ts
|
|
3
|
+
const reservedFields = ["id", "etag"];
|
|
4
|
+
const isFieldType = (type) => definitionFieldTypes.includes(type);
|
|
5
|
+
const fieldProblem = (field, columns) => {
|
|
6
|
+
if (!isUserTableName(field.name)) return `the field name ${JSON.stringify(field.name)} is not lowercase letters, digits and "_", starting with a letter, at most 63 bytes`;
|
|
7
|
+
if (reservedFields.includes(field.name)) return `the field name "${field.name}" is reserved`;
|
|
8
|
+
if (!isFieldType(field.type)) return `the field "${field.name}" has the type ${JSON.stringify(field.type)}, which is not one of ${definitionFieldTypes.join(", ")}`;
|
|
9
|
+
if (field.type === "decimal" && (field.precision === null || field.scale === null)) return `the decimal field "${field.name}" has no precision and scale`;
|
|
10
|
+
const column = columns.get(field.name);
|
|
11
|
+
if (!column) return `the column "${field.name}" does not exist`;
|
|
12
|
+
if (!definitionColumnTypes[field.type].includes(column.type)) return `the column "${field.name}" is ${column.type}, not what a ${field.type} field needs (${definitionColumnTypes[field.type].join(" or ")})`;
|
|
13
|
+
if (!field.required && column.notNull) return `the optional field "${field.name}" has a NOT NULL column`;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The definition checked against the user tables schema, or the first thing wrong with it: every name a safe
|
|
17
|
+
* identifier, every type one of the eight, its table there with an `id bigint` primary key and a column of the right
|
|
18
|
+
* type for each field, and its title a text field of it.
|
|
19
|
+
*/
|
|
20
|
+
const checkDefinition = (definition, tables) => {
|
|
21
|
+
const problem = (text) => ({ problem: `The user table ${JSON.stringify(definition.name)} is not served: ${text}.` });
|
|
22
|
+
if (!isUserTableName(definition.name)) return problem("its name is not lowercase letters, digits and \"_\", starting with a letter, at most 63 bytes");
|
|
23
|
+
const columns = tables.get(definition.name);
|
|
24
|
+
if (!columns) return problem(`the table ${userTablesSchema}.${definition.name} does not exist`);
|
|
25
|
+
const id = columns.get("id");
|
|
26
|
+
if (!id || id.type !== "bigint" || !id.key) return problem(`${userTablesSchema}.${definition.name} has no "id" bigint primary key`);
|
|
27
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const field of definition.fields) {
|
|
29
|
+
if (seen.has(field.name)) return problem(`the field "${field.name}" is defined twice`);
|
|
30
|
+
seen.add(field.name);
|
|
31
|
+
const found = fieldProblem(field, columns);
|
|
32
|
+
if (found) return problem(found);
|
|
33
|
+
}
|
|
34
|
+
const title = definition.fields.find((field) => field.name === definition.title_field);
|
|
35
|
+
if (definition.title_field !== null && (!title || title.type !== "text" && title.type !== "long_text")) return problem(`its title "${definition.title_field}" is not one of its text fields`);
|
|
36
|
+
return { definition: {
|
|
37
|
+
...definition,
|
|
38
|
+
fields: definition.fields
|
|
39
|
+
} };
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { checkDefinition };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CheckedDefinition } from "./check-definition.js";
|
|
2
|
+
import { TableIndexes } from "@protobase/query";
|
|
3
|
+
//#region src/user-tables/definition-resource.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The resource and view of a checked definition over `user_tables.<name>`, with the indexes its table has (none before
|
|
6
|
+
* they are read): built with the schema builders, so it is checked, accessed and guarded as a resource of the config.
|
|
7
|
+
*/
|
|
8
|
+
export declare const definitionResource: (definition: CheckedDefinition, indexes?: TableIndexes) => {
|
|
9
|
+
resource: import("@protobase/schema").ResourceBuilder<any, import("@protobase/schema").Ref<string, unknown>>;
|
|
10
|
+
view: import("@protobase/schema").ViewBuilder<import("@protobase/schema").ResourceBuilder<any, import("@protobase/schema").Ref<string, unknown>>>;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { userTablesSchema } from "./names.js";
|
|
2
|
+
import { f, resource, view } from "@protobase/schema";
|
|
3
|
+
import { indexServes } from "@protobase/query";
|
|
4
|
+
//#region src/user-tables/definition-resource.ts
|
|
5
|
+
const admins = ({ user }) => user?.roles?.includes("admin") ?? false;
|
|
6
|
+
const builders = {
|
|
7
|
+
text: () => f.text(),
|
|
8
|
+
long_text: () => f.text(),
|
|
9
|
+
integer: () => f.integer(),
|
|
10
|
+
decimal: (field) => f.decimal({
|
|
11
|
+
precision: field.precision,
|
|
12
|
+
scale: field.scale
|
|
13
|
+
}),
|
|
14
|
+
boolean: () => f.boolean(),
|
|
15
|
+
date: () => f.date(),
|
|
16
|
+
timestamp: () => f.timestamp(),
|
|
17
|
+
json: () => f.json()
|
|
18
|
+
};
|
|
19
|
+
const served = (indexes, column, need) => indexes.indexes.some((index) => indexServes(index, column, need, /* @__PURE__ */ new Set()));
|
|
20
|
+
const isText = (field) => field.type === "text" || field.type === "long_text";
|
|
21
|
+
const fieldOf = (field, indexes) => {
|
|
22
|
+
const built = builders[field.type](field);
|
|
23
|
+
const optional = field.required ? built : built.optional();
|
|
24
|
+
if (field.type === "json") return optional;
|
|
25
|
+
const sortable = served(indexes, field.name, "btree");
|
|
26
|
+
const filtered = sortable || isText(field) && served(indexes, field.name, "trigram") ? optional.filterable() : optional;
|
|
27
|
+
return sortable ? filtered.sortable() : filtered;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The resource and view of a checked definition over `user_tables.<name>`, with the indexes its table has (none before
|
|
31
|
+
* they are read): built with the schema builders, so it is checked, accessed and guarded as a resource of the config.
|
|
32
|
+
*/
|
|
33
|
+
const definitionResource = (definition, indexes = {
|
|
34
|
+
kind: "table",
|
|
35
|
+
indexes: []
|
|
36
|
+
}) => {
|
|
37
|
+
const key = f.bigint().readOnly().dbDefault();
|
|
38
|
+
const keyed = served(indexes, "id", "btree") ? key.filterable().sortable() : key;
|
|
39
|
+
const fields = Object.fromEntries([["id", keyed], ...definition.fields.map((field) => [field.name, fieldOf(field, indexes)])]);
|
|
40
|
+
const title = definition.fields.find((field) => field.name === definition.title_field);
|
|
41
|
+
const base = resource(definition.name).table(`${userTablesSchema}.${definition.name}`).fields(fields).primaryKey((r) => r.id).explicitGrants().access({
|
|
42
|
+
list: admins,
|
|
43
|
+
read: admins,
|
|
44
|
+
create: admins,
|
|
45
|
+
update: admins,
|
|
46
|
+
delete: admins
|
|
47
|
+
});
|
|
48
|
+
const searchable = title && served(indexes, title.name, "trigram") ? base.search((r) => [r[title.name]]) : base;
|
|
49
|
+
const labelled = view(definition.name).names({
|
|
50
|
+
singular: definition.singular_label,
|
|
51
|
+
plural: definition.plural_label
|
|
52
|
+
}).fields((r) => Object.fromEntries(definition.fields.map((field) => {
|
|
53
|
+
const labelledField = r[field.name].label(field.label);
|
|
54
|
+
return [field.name, field.type === "long_text" ? labelledField.multiline() : labelledField];
|
|
55
|
+
})));
|
|
56
|
+
return {
|
|
57
|
+
resource: searchable,
|
|
58
|
+
view: title ? labelled.title((r) => r[title.name]) : labelled
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { definitionResource };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Db } from "@protobase/query";
|
|
2
|
+
//#region src/user-tables/definitions-version.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The version of the stored definitions as a whole, from their rows alone: how many there are, the highest id and the
|
|
5
|
+
* sum of their versions. Adding one raises the highest id, which an identity never gives again, removing one lowers the
|
|
6
|
+
* count, and changing one raises its version, so each change moves it, as long as every change of a definition or its
|
|
7
|
+
* fields raises the definition's version. One read of `tables`, which holds a row per table users add.
|
|
8
|
+
*/
|
|
9
|
+
export declare const readDefinitionsVersion: (db: Db) => Promise<string>;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { definitionsSchema } from "./names.js";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
//#region src/user-tables/definitions-version.ts
|
|
4
|
+
/**
|
|
5
|
+
* The version of the stored definitions as a whole, from their rows alone: how many there are, the highest id and the
|
|
6
|
+
* sum of their versions. Adding one raises the highest id, which an identity never gives again, removing one lowers the
|
|
7
|
+
* count, and changing one raises its version, so each change moves it, as long as every change of a definition or its
|
|
8
|
+
* fields raises the definition's version. One read of `tables`, which holds a row per table users add.
|
|
9
|
+
*/
|
|
10
|
+
const readDefinitionsVersion = async (db) => {
|
|
11
|
+
const { rows } = await sql`select count(*) || ':' || coalesce(max(id), 0) || ':' || coalesce(sum(version), 0) as version
|
|
12
|
+
from ${sql.table(`${definitionsSchema}.tables`)}`.execute(db);
|
|
13
|
+
return rows[0].version;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { readDefinitionsVersion };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/user-tables/latest-admin.ts
|
|
2
|
+
/** How long a read of the definitions version holds before a request reads it again, in milliseconds. */
|
|
3
|
+
const versionCheckMs = 1e3;
|
|
4
|
+
/**
|
|
5
|
+
* The latest admin built, the only one kept: a cache of one entry, swapped for one built anew when the definitions
|
|
6
|
+
* version moved. `get(true)` reads the version at once, `get()` at most once a `versionCheckMs`; either waits for a read
|
|
7
|
+
* or rebuild already running instead of starting another, so two requests never build twice. A request keeps the admin
|
|
8
|
+
* it got until it is answered.
|
|
9
|
+
*/
|
|
10
|
+
const latestAdmin = ({ admin, version, readVersion, rebuild, now = Date.now }) => {
|
|
11
|
+
let latest = {
|
|
12
|
+
admin,
|
|
13
|
+
version,
|
|
14
|
+
checkedAt: now()
|
|
15
|
+
};
|
|
16
|
+
let checking;
|
|
17
|
+
const check = async () => {
|
|
18
|
+
const checkedAt = now();
|
|
19
|
+
const read = await readVersion();
|
|
20
|
+
if (read === latest.version) {
|
|
21
|
+
latest = {
|
|
22
|
+
...latest,
|
|
23
|
+
checkedAt
|
|
24
|
+
};
|
|
25
|
+
return latest.admin;
|
|
26
|
+
}
|
|
27
|
+
latest = {
|
|
28
|
+
admin: await rebuild(latest.admin),
|
|
29
|
+
version: read,
|
|
30
|
+
checkedAt
|
|
31
|
+
};
|
|
32
|
+
return latest.admin;
|
|
33
|
+
};
|
|
34
|
+
const get = (atOnce = false) => {
|
|
35
|
+
if (!atOnce && now() - latest.checkedAt < 1e3) return Promise.resolve(latest.admin);
|
|
36
|
+
checking ??= check().finally(() => {
|
|
37
|
+
checking = void 0;
|
|
38
|
+
});
|
|
39
|
+
return checking;
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
get,
|
|
43
|
+
current: () => latest.admin
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { latestAdmin, versionCheckMs };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/user-tables/names.d.ts
|
|
2
|
+
/** The schema the definitions of the tables users add live in: `tables`, and `fields` of each. */
|
|
3
|
+
export declare const definitionsSchema = "table_definitions";
|
|
4
|
+
/** The schema the tables users add live in, apart from their definitions, so no name of one reaches a definitions table. */
|
|
5
|
+
export declare const userTablesSchema = "user_tables";
|
|
6
|
+
/** The field types a definition may give, each with the Postgres types its column may have. */
|
|
7
|
+
export declare const definitionColumnTypes: {
|
|
8
|
+
readonly text: readonly ["text"];
|
|
9
|
+
readonly long_text: readonly ["text"];
|
|
10
|
+
readonly integer: readonly ["integer"];
|
|
11
|
+
readonly decimal: readonly ["numeric"];
|
|
12
|
+
readonly boolean: readonly ["boolean"];
|
|
13
|
+
readonly date: readonly ["date"];
|
|
14
|
+
readonly timestamp: readonly ["timestamp with time zone"];
|
|
15
|
+
readonly json: readonly ["jsonb"];
|
|
16
|
+
};
|
|
17
|
+
export type DefinitionFieldType = keyof typeof definitionColumnTypes;
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/user-tables/names.ts
|
|
2
|
+
/** The schema the definitions of the tables users add live in: `tables`, and `fields` of each. */
|
|
3
|
+
const definitionsSchema = "table_definitions";
|
|
4
|
+
/** The schema the tables users add live in, apart from their definitions, so no name of one reaches a definitions table. */
|
|
5
|
+
const userTablesSchema = "user_tables";
|
|
6
|
+
/** What a name of a user table or field must be: it reaches SQL as an identifier, always quoted too. */
|
|
7
|
+
const userTableName = /^[a-z][a-z0-9_]*$/;
|
|
8
|
+
/** The field types a definition may give, each with the Postgres types its column may have. */
|
|
9
|
+
const definitionColumnTypes = {
|
|
10
|
+
text: ["text"],
|
|
11
|
+
long_text: ["text"],
|
|
12
|
+
integer: ["integer"],
|
|
13
|
+
decimal: ["numeric"],
|
|
14
|
+
boolean: ["boolean"],
|
|
15
|
+
date: ["date"],
|
|
16
|
+
timestamp: ["timestamp with time zone"],
|
|
17
|
+
json: ["jsonb"]
|
|
18
|
+
};
|
|
19
|
+
const definitionFieldTypes = Object.keys(definitionColumnTypes);
|
|
20
|
+
const isUserTableName = (name) => userTableName.test(name) && new TextEncoder().encode(name).length <= 63;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { definitionColumnTypes, definitionFieldTypes, definitionsSchema, isUserTableName, userTableName, userTablesSchema };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Db } from "@protobase/query";
|
|
2
|
+
//#region src/user-tables/read-definitions.d.ts
|
|
3
|
+
/** A field of a stored definition, as its row holds it: nothing in it is trusted before `checkDefinition`. */
|
|
4
|
+
export type StoredField = {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
type: string;
|
|
8
|
+
required: boolean;
|
|
9
|
+
precision: number | null;
|
|
10
|
+
scale: number | null;
|
|
11
|
+
};
|
|
12
|
+
/** A stored definition of a table users add, as its rows hold it, with its fields in order. */
|
|
13
|
+
export type StoredDefinition = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
singular_label: string;
|
|
17
|
+
plural_label: string;
|
|
18
|
+
title_field: string | null;
|
|
19
|
+
version: string;
|
|
20
|
+
fields: StoredField[];
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { definitionsSchema, userTablesSchema } from "./names.js";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
//#region src/user-tables/read-definitions.ts
|
|
4
|
+
/** The stored definitions, by id, each with its fields by position. */
|
|
5
|
+
const readDefinitions = async (db) => {
|
|
6
|
+
const { rows } = await sql`select t.id::text as id, t.name, t.singular_label, t.plural_label, t.title_field, t.version::text as version,
|
|
7
|
+
coalesce((select json_agg(json_build_object('name', f.name, 'label', f.label, 'type', f.type, 'required', f.required, 'precision', f.precision, 'scale', f.scale) order by f.position, f.name)
|
|
8
|
+
from ${sql.table(`${definitionsSchema}.fields`)} f where f.table_id = t.id), '[]') as fields
|
|
9
|
+
from ${sql.table(`${definitionsSchema}.tables`)} t order by t.id`.execute(db);
|
|
10
|
+
return rows;
|
|
11
|
+
};
|
|
12
|
+
/** The columns of the tables of the user tables schema, from the catalogue. */
|
|
13
|
+
const readUserTableColumns = async (db) => {
|
|
14
|
+
const { rows } = await sql`select c.relname::text as table, a.attname::text as column,
|
|
15
|
+
format_type(a.atttypid, null) as type, a.attnotnull as not_null,
|
|
16
|
+
exists (select from pg_index i where i.indrelid = c.oid and i.indisprimary and i.indnatts = 1 and i.indkey[0] = a.attnum) as key
|
|
17
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace join pg_attribute a on a.attrelid = c.oid
|
|
18
|
+
where n.nspname = ${userTablesSchema} and c.relkind in ('r', 'p') and a.attnum > 0 and not a.attisdropped
|
|
19
|
+
order by c.relname, a.attnum`.execute(db);
|
|
20
|
+
const tables = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
const columns = tables.get(row.table) ?? /* @__PURE__ */ new Map();
|
|
23
|
+
columns.set(row.column, {
|
|
24
|
+
type: row.type,
|
|
25
|
+
notNull: row.not_null,
|
|
26
|
+
key: row.key
|
|
27
|
+
});
|
|
28
|
+
tables.set(row.table, columns);
|
|
29
|
+
}
|
|
30
|
+
return tables;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { readDefinitions, readUserTableColumns };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { FieldDecrypter } from "../encryption/decrypt-field-types.js";
|
|
2
|
+
import { JobDefinition } from "../jobs/job-types.js";
|
|
3
|
+
import { RealtimePrune } from "../realtime/realtime-prune.js";
|
|
4
|
+
import { Admin, AdminEnv } from "../types.js";
|
|
5
|
+
import { UserTables } from "./user-table-resources.js";
|
|
6
|
+
import "../index.js";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
//#region src/user-tables/reloading-admin.d.ts
|
|
9
|
+
export type ReloadingAdminInput = {
|
|
10
|
+
/** The admin startup built, from the user tables of `version`. */
|
|
11
|
+
first: Admin;
|
|
12
|
+
version: string;
|
|
13
|
+
/** The definitions version now (`readDefinitionsVersion`). */
|
|
14
|
+
readVersion: () => Promise<string>;
|
|
15
|
+
/** The user tables now (`userTableResources`). */
|
|
16
|
+
readTables: () => Promise<UserTables>;
|
|
17
|
+
/** The admin of the app with these user tables, as startup built `first`. */
|
|
18
|
+
build: (tables: UserTables) => Admin;
|
|
19
|
+
/** The app's `onUnhandledError`: a failed version read, and an admin that does not build. */
|
|
20
|
+
report?: (error: unknown, request?: Request) => void;
|
|
21
|
+
statementTimeoutMs?: number;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The app's admin, rebuilt when the tables users add change, so every process serves a new definition without a
|
|
26
|
+
* restart. `/meta` reads the definitions version first; any other request at most once a second, and once more when the
|
|
27
|
+
* admin answers it not found or invalid, as it does a resource or field it does not know, and then is answered again by
|
|
28
|
+
* the admin built anew. A request finishes on the admin it started with, and a WebSocket keeps the admin it opened
|
|
29
|
+
* with. An admin that does not build with the new definitions is reported, and the one before keeps serving.
|
|
30
|
+
* `reload()` reads the version at once, for the process that changed a definition. The jobs, the realtime prune and
|
|
31
|
+
* the field decrypter are those of `first`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const reloadingAdmin: ({ first, version, readVersion, readTables, build, report, statementTimeoutMs, now }: ReloadingAdminInput) => Hono<AdminEnv, import("hono/types").BlankSchema, "/"> & {
|
|
34
|
+
jobs: JobDefinition[];
|
|
35
|
+
startRealtimePrune: () => RealtimePrune;
|
|
36
|
+
decryptField: FieldDecrypter;
|
|
37
|
+
metaPath: string;
|
|
38
|
+
reload: () => Promise<undefined>;
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createErrorHandler } from "../error-handler.js";
|
|
2
|
+
import { latestAdmin } from "./latest-admin.js";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
//#region src/user-tables/reloading-admin.ts
|
|
5
|
+
const problemStatuses = /* @__PURE__ */ new Set([400, 404]);
|
|
6
|
+
const namesUnknown = (response) => problemStatuses.has(response.status) && (response.headers.get("content-type")?.startsWith("application/problem+json") ?? false);
|
|
7
|
+
const replayable = async (request) => {
|
|
8
|
+
if (!request.body) return () => request;
|
|
9
|
+
if (!request.headers.get("content-type")?.includes("json")) return;
|
|
10
|
+
const body = await request.arrayBuffer();
|
|
11
|
+
return () => new Request(request, { body });
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The app's admin, rebuilt when the tables users add change, so every process serves a new definition without a
|
|
15
|
+
* restart. `/meta` reads the definitions version first; any other request at most once a second, and once more when the
|
|
16
|
+
* admin answers it not found or invalid, as it does a resource or field it does not know, and then is answered again by
|
|
17
|
+
* the admin built anew. A request finishes on the admin it started with, and a WebSocket keeps the admin it opened
|
|
18
|
+
* with. An admin that does not build with the new definitions is reported, and the one before keeps serving.
|
|
19
|
+
* `reload()` reads the version at once, for the process that changed a definition. The jobs, the realtime prune and
|
|
20
|
+
* the field decrypter are those of `first`.
|
|
21
|
+
*/
|
|
22
|
+
const reloadingAdmin = ({ first, version, readVersion, readTables, build, report, statementTimeoutMs, now }) => {
|
|
23
|
+
const rebuild = async (current) => {
|
|
24
|
+
const tables = await readTables();
|
|
25
|
+
try {
|
|
26
|
+
return build(tables);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
report?.(new Error("The tables users add changed, but the app does not build with them, so it serves the tables it served before", { cause: error }));
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const latest = latestAdmin({
|
|
33
|
+
admin: first,
|
|
34
|
+
version,
|
|
35
|
+
readVersion,
|
|
36
|
+
rebuild,
|
|
37
|
+
...now && { now }
|
|
38
|
+
});
|
|
39
|
+
const answer = async (request, env) => {
|
|
40
|
+
const path = new URL(request.url).pathname;
|
|
41
|
+
const atOnce = path === first.metaPath || path.startsWith(`${first.metaPath}/`);
|
|
42
|
+
const admin = await latest.get(atOnce);
|
|
43
|
+
const again = atOnce ? void 0 : await replayable(request);
|
|
44
|
+
if (!again) return admin.fetch(request, env);
|
|
45
|
+
const answered = await admin.fetch(again(), env);
|
|
46
|
+
if (!namesUnknown(answered)) return answered;
|
|
47
|
+
const fresh = await latest.get(true);
|
|
48
|
+
return fresh === admin ? answered : fresh.fetch(again(), env);
|
|
49
|
+
};
|
|
50
|
+
const app = new Hono();
|
|
51
|
+
app.onError(createErrorHandler(report, statementTimeoutMs));
|
|
52
|
+
app.all("*", (c) => answer(c.req.raw, c.env));
|
|
53
|
+
const reload = async () => void await latest.get(true);
|
|
54
|
+
return Object.assign(app, {
|
|
55
|
+
jobs: first.jobs,
|
|
56
|
+
startRealtimePrune: first.startRealtimePrune,
|
|
57
|
+
decryptField: first.decryptField,
|
|
58
|
+
metaPath: first.metaPath,
|
|
59
|
+
reload
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { reloadingAdmin };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { definitionFieldTypes, definitionsSchema, userTableName, userTablesSchema } from "./names.js";
|
|
2
|
+
//#region src/user-tables/schema-sql.ts
|
|
3
|
+
const nameCheck = (column) => `check (${column} ~ '${userTableName.source}' and octet_length(${column}) <= 63)`;
|
|
4
|
+
/** The table of definitions: one row per table users add, its labels and a version that each change of it raises. */
|
|
5
|
+
const tablesSql = () => `create table ${definitionsSchema}.tables (
|
|
6
|
+
id bigint generated always as identity primary key,
|
|
7
|
+
name text not null unique ${nameCheck("name")},
|
|
8
|
+
singular_label text not null,
|
|
9
|
+
plural_label text not null,
|
|
10
|
+
title_field text ${nameCheck("title_field")},
|
|
11
|
+
version bigint not null check (version > 0)
|
|
12
|
+
)`;
|
|
13
|
+
/** The fields of each definition, in order, each with its label and type; a decimal names its precision and scale. */
|
|
14
|
+
const fieldsSql = () => `create table ${definitionsSchema}.fields (
|
|
15
|
+
table_id bigint not null references ${definitionsSchema}.tables (id) on delete cascade,
|
|
16
|
+
name text not null ${nameCheck("name")} check (name not in ('id', 'etag')),
|
|
17
|
+
label text not null,
|
|
18
|
+
type text not null check (type in (${definitionFieldTypes.map((type) => `'${type}'`).join(", ")})),
|
|
19
|
+
required boolean not null,
|
|
20
|
+
position integer not null,
|
|
21
|
+
precision integer check (precision between 1 and 1000),
|
|
22
|
+
scale integer check (scale between 0 and precision),
|
|
23
|
+
primary key (table_id, name),
|
|
24
|
+
check ((type = 'decimal') = (precision is not null and scale is not null))
|
|
25
|
+
)`;
|
|
26
|
+
const schemasSql = () => [
|
|
27
|
+
"create extension if not exists pg_trgm",
|
|
28
|
+
`create schema if not exists ${definitionsSchema}`,
|
|
29
|
+
`revoke create on schema ${definitionsSchema} from public`,
|
|
30
|
+
`create schema if not exists ${userTablesSchema}`,
|
|
31
|
+
`revoke create on schema ${userTablesSchema} from public`,
|
|
32
|
+
`grant usage, create on schema ${userTablesSchema} to current_user`
|
|
33
|
+
];
|
|
34
|
+
//#endregion
|
|
35
|
+
export { fieldsSql, schemasSql, tablesSql };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Db } from "@protobase/query";
|
|
2
|
+
//#region src/user-tables/schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SQL that adds what user tables need: pg_trgm, the definitions schema with its `tables` and `fields`, and the
|
|
5
|
+
* schema the tables themselves live in. `undefined` when current. Nothing is dropped.
|
|
6
|
+
*/
|
|
7
|
+
export declare const userTablesMigration: (db: Db) => Promise<string | undefined>;
|
|
8
|
+
/** Refuses to go on while the schemas of user tables are missing; the project's migrations add them. */
|
|
9
|
+
export declare const checkUserTablesSchema: (db: Db) => Promise<void>;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { migrationCommand } from "../changes/capture-schema.js";
|
|
2
|
+
import { definitionsSchema, userTablesSchema } from "./names.js";
|
|
3
|
+
import { fieldsSql, schemasSql, tablesSql } from "./schema-sql.js";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
//#region src/user-tables/schema.ts
|
|
6
|
+
const readUserTablesSchema = async (db) => {
|
|
7
|
+
const { rows } = await sql`select
|
|
8
|
+
to_regclass(${`${definitionsSchema}.tables`}) is not null as tables,
|
|
9
|
+
to_regclass(${`${definitionsSchema}.fields`}) is not null as fields,
|
|
10
|
+
exists (select from pg_namespace where nspname = ${userTablesSchema}) as data,
|
|
11
|
+
exists (select from pg_extension where extname = 'pg_trgm') as trgm`.execute(db);
|
|
12
|
+
return rows[0];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The SQL that adds what user tables need: pg_trgm, the definitions schema with its `tables` and `fields`, and the
|
|
16
|
+
* schema the tables themselves live in. `undefined` when current. Nothing is dropped.
|
|
17
|
+
*/
|
|
18
|
+
const userTablesMigration = async (db) => {
|
|
19
|
+
const found = await readUserTablesSchema(db);
|
|
20
|
+
if (found.tables && found.fields && found.data && found.trgm) return void 0;
|
|
21
|
+
return `${[
|
|
22
|
+
...schemasSql(),
|
|
23
|
+
...found.tables ? [] : [tablesSql()],
|
|
24
|
+
...found.fields ? [] : [fieldsSql()]
|
|
25
|
+
].join(";\n\n")};`;
|
|
26
|
+
};
|
|
27
|
+
/** Refuses to go on while the schemas of user tables are missing; the project's migrations add them. */
|
|
28
|
+
const checkUserTablesSchema = async (db) => {
|
|
29
|
+
const found = await readUserTablesSchema(db);
|
|
30
|
+
if (found.tables && found.fields && found.data && found.trgm) return;
|
|
31
|
+
throw new Error(`The ${definitionsSchema} or ${userTablesSchema} schema, or pg_trgm, is missing, and userTables keeps the tables users add there. Write it as a migration with \`${migrationCommand}\`, apply it with the project's migrations, then start again.`);
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { checkUserTablesSchema, userTablesMigration };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ViewSource } from "../types.js";
|
|
2
|
+
import { ResourceSource } from "../resource-source.js";
|
|
3
|
+
//#region src/user-tables/served-user-tables.d.ts
|
|
4
|
+
/** The resources and views of the tables users add, as `userTableResources` builds them. */
|
|
5
|
+
export type UserTableSources = {
|
|
6
|
+
resources: ResourceSource[];
|
|
7
|
+
views: ViewSource[];
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/user-tables/served-user-tables.ts
|
|
2
|
+
/**
|
|
3
|
+
* The user tables a name of the app's own or a built-in resource does not take, and a warning for each one it does: a
|
|
4
|
+
* resource in code of the same name wins, which is how a table taken over as code hands over. Of two with one name, the
|
|
5
|
+
* first is kept.
|
|
6
|
+
*/
|
|
7
|
+
const servedUserTables = (tables, taken) => {
|
|
8
|
+
const names = new Set(taken);
|
|
9
|
+
const kept = {
|
|
10
|
+
resources: [],
|
|
11
|
+
views: []
|
|
12
|
+
};
|
|
13
|
+
const warnings = [];
|
|
14
|
+
for (const source of tables.resources) {
|
|
15
|
+
const { name } = source.toModel();
|
|
16
|
+
if (names.has(name)) {
|
|
17
|
+
warnings.push(`The user table "${name}" is not served: a resource of the app has its name.`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
names.add(name);
|
|
21
|
+
kept.resources.push(source);
|
|
22
|
+
kept.views.push(...tables.views.filter((view) => view.toModel().resource === name).slice(0, 1));
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...kept,
|
|
26
|
+
warnings
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { servedUserTables };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { definitionResource } from "./definition-resource.js";
|
|
2
|
+
import { Db } from "@protobase/query";
|
|
3
|
+
//#region src/user-tables/user-table-resources.d.ts
|
|
4
|
+
/** What the stored definitions serve: a resource and view for each that fits the database, and why each other one is left out. */
|
|
5
|
+
export type UserTables = {
|
|
6
|
+
resources: Array<ReturnType<typeof definitionResource>['resource']>;
|
|
7
|
+
views: Array<ReturnType<typeof definitionResource>['view']>;
|
|
8
|
+
problems: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The resources of the tables users add, from their definitions and the catalogue: a definition that does not fit the
|
|
12
|
+
* database is left out with a problem, so the rest of the app serves. Filters, sorts and search are claimed only where
|
|
13
|
+
* the table's indexes serve them, read as the scan guard reads them.
|
|
14
|
+
*/
|
|
15
|
+
export declare const userTableResources: (db: Db) => Promise<UserTables>;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { checkDefinition } from "./check-definition.js";
|
|
2
|
+
import { definitionResource } from "./definition-resource.js";
|
|
3
|
+
import { readDefinitions, readUserTableColumns } from "./read-definitions.js";
|
|
4
|
+
import { tableIndexes } from "@protobase/query";
|
|
5
|
+
//#region src/user-tables/user-table-resources.ts
|
|
6
|
+
/**
|
|
7
|
+
* The resources of the tables users add, from their definitions and the catalogue: a definition that does not fit the
|
|
8
|
+
* database is left out with a problem, so the rest of the app serves. Filters, sorts and search are claimed only where
|
|
9
|
+
* the table's indexes serve them, read as the scan guard reads them.
|
|
10
|
+
*/
|
|
11
|
+
const userTableResources = async (db) => {
|
|
12
|
+
const [definitions, columns] = await Promise.all([readDefinitions(db), readUserTableColumns(db)]);
|
|
13
|
+
const tables = {
|
|
14
|
+
resources: [],
|
|
15
|
+
views: [],
|
|
16
|
+
problems: []
|
|
17
|
+
};
|
|
18
|
+
for (const stored of definitions) {
|
|
19
|
+
const checked = checkDefinition(stored, columns);
|
|
20
|
+
if ("problem" in checked) {
|
|
21
|
+
tables.problems.push(checked.problem);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const indexes = await tableIndexes(db, definitionResource(checked.definition).resource.toModel());
|
|
25
|
+
const { resource, view } = definitionResource(checked.definition, indexes);
|
|
26
|
+
tables.resources.push(resource);
|
|
27
|
+
tables.views.push(view);
|
|
28
|
+
}
|
|
29
|
+
return tables;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { userTableResources };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.67.
|
|
3
|
+
"version": "0.67.2",
|
|
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": {
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"@better-auth/sso": "^1.7.12",
|
|
44
44
|
"@livekit/protocol": "1.53.1",
|
|
45
45
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
46
|
-
"@protobase/layout": "0.67.
|
|
47
|
-
"@protobase/query": "0.67.
|
|
48
|
-
"@protobase/schema": "0.67.
|
|
46
|
+
"@protobase/layout": "0.67.2",
|
|
47
|
+
"@protobase/query": "0.67.2",
|
|
48
|
+
"@protobase/schema": "0.67.2",
|
|
49
49
|
"@react-email/render": "^2.1.0",
|
|
50
50
|
"@react-pdf/primitives": "^4.4.0",
|
|
51
51
|
"@react-pdf/renderer": "^4.9.0",
|