@protobase/server 0.67.2 → 0.67.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1010,8 +1010,8 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
1010
1010
  $Infer: {
1011
1011
  body: {
1012
1012
  permissions: {
1013
- readonly user?: ("list" | "create" | "update" | "delete" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1014
- readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1013
+ readonly user?: ("create" | "update" | "delete" | "list" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1014
+ readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1015
1015
  };
1016
1016
  } & {
1017
1017
  userId?: string | undefined;
@@ -1143,8 +1143,8 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
1143
1143
  $Infer: {
1144
1144
  body: {
1145
1145
  permissions: {
1146
- readonly user?: ("list" | "create" | "update" | "delete" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1147
- readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1146
+ readonly user?: ("create" | "update" | "delete" | "list" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1147
+ readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1148
1148
  };
1149
1149
  } & {
1150
1150
  userId?: string | undefined;
@@ -17,9 +17,11 @@ import { Hono } from "hono";
17
17
  * with their labels, `membership` for those a member can hold, and the roles each one's holders may also give; and
18
18
  * `anonymous` when the project's anonymous role reads a resource, so the admin app opens for visitors before anyone signs in; `mcp` when MCP clients can connect, with the `url` of its endpoint, so the account page shows that address and lists the connected clients; `personalMail`, the mail providers people may link their mailbox with, each with the sign-in provider it links through, the scopes and the authorization parameters of the link, so the account page offers "Link Gmail"; and
19
19
  * `plugins`, those the app loads before sign-in (see `PluginOption`); and `locale`, the language `options.locale` puts
20
- * the screens in, so the sign-in screens and public forms, which come before `/meta`, are in it too.
20
+ * the screens in, so the sign-in screens and public forms, which come before `/meta`, are in it too; `appName` and
21
+ * `appIcon`, the app's name and the path of its own icon when it has one, which those screens show as `/meta` names them;
22
+ * and `runsOnNote` when the host has the sign-in page end on the "Runs on Protobase" note (see `readRunsOnNote`).
21
23
  */
22
- const statusRoute = (auth, { anonymous = false, plugins = [], locale, firstAdminUrl } = {}) => {
24
+ const statusRoute = (auth, { anonymous = false, plugins = [], locale, firstAdminUrl, appName, appIcon, runsOnNote = false } = {}) => {
23
25
  const app = new Hono();
24
26
  app.get("/status", async (c) => {
25
27
  const context = await auth.$context;
@@ -60,6 +62,9 @@ const statusRoute = (auth, { anonymous = false, plugins = [], locale, firstAdmin
60
62
  })) },
61
63
  ...plugins.length > 0 && { plugins },
62
64
  ...locale && { locale },
65
+ ...appName && { appName },
66
+ ...appIcon && { appIcon },
67
+ ...runsOnNote && { runsOnNote },
63
68
  ...auth.organizations && { organizations: {
64
69
  create: auth.organizations.create,
65
70
  roles: roleLabels(auth.roleDefinitions)
@@ -1,6 +1,9 @@
1
1
  import { configExports } from "./resource-source.js";
2
+ import { ownedTableNames } from "./user-tables/owned-definitions.js";
2
3
  import { definitionsSchema, userTablesSchema } from "./user-tables/names.js";
4
+ import { CheckedDefinition } from "./user-tables/check-definition.js";
5
+ import { maxTextLength } from "./user-tables/definition-resource.js";
3
6
  import { userTableResources } from "./user-tables/user-table-resources.js";
4
7
  import { builtInResources } from "./built-in-resources.js";
5
8
  import { webhooksModule } from "./webhooks/table/webhooks-module.js";
6
- export { builtInResources, configExports, definitionsSchema, userTableResources, userTablesSchema, webhooksModule };
9
+ export { type CheckedDefinition, builtInResources, configExports, definitionsSchema, maxTextLength, ownedTableNames, userTableResources, userTablesSchema, webhooksModule };
@@ -1,6 +1,8 @@
1
1
  import { builtInResources } from "./built-in-resources.js";
2
- import { configExports } from "./resource-source.js";
3
2
  import { definitionsSchema, userTablesSchema } from "./user-tables/names.js";
3
+ import { maxTextLength } from "./user-tables/definition-resource.js";
4
+ import { configExports } from "./resource-source.js";
5
+ import { ownedTableNames } from "./user-tables/owned-definitions.js";
4
6
  import { userTableResources } from "./user-tables/user-table-resources.js";
5
7
  import { webhooksModule } from "./webhooks/table/webhooks-module.js";
6
- export { builtInResources, configExports, definitionsSchema, userTableResources, userTablesSchema, webhooksModule };
8
+ export { builtInResources, configExports, definitionsSchema, maxTextLength, ownedTableNames, userTableResources, userTablesSchema, webhooksModule };
@@ -108,11 +108,12 @@ import { metaRoutes } from "./routes/meta.js";
108
108
  import { faviconPath } from "./web-app/web-manifest.js";
109
109
  import { webManifestRoute } from "./web-app/web-manifest-route.js";
110
110
  import { mcpIcons } from "./web-app/mcp-icons.js";
111
+ import { readRunsOnNote } from "./runs-on-note.js";
111
112
  import { recordRoutes } from "./routes/record.js";
112
113
  import { firstAdminUrl } from "./better-auth/first-admin-url.js";
113
114
  import { createScanGuard } from "./scan-guard.js";
114
115
  import { builtInResources } from "./built-in-resources.js";
115
- import { servedUserTables } from "./user-tables/served-user-tables.js";
116
+ import { adminUserTables } from "./user-tables/admin-user-tables.js";
116
117
  import { Hono } from "hono";
117
118
  import { accountPage, accountPageName, checkPages, settingsResources, withSettingsFields } from "@protobase/layout";
118
119
  import { guestCallsPath, ringsPath, statusPath } from "@protobase/schema/internal";
@@ -137,10 +138,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
137
138
  notifications: notifications !== void 0,
138
139
  ...events && { events }
139
140
  });
140
- const served = servedUserTables(userTables ?? {
141
- resources: [],
142
- views: []
143
- }, [...appModels, ...builtIn.resources.map((resource) => resource.toModel())].map((model) => model.name));
141
+ const served = adminUserTables(userTables, [...appModels, ...builtIn.resources.map((resource) => resource.toModel())].map((model) => model.name), options.roles !== void 0);
144
142
  for (const warning of served.warnings) console.warn(warning);
145
143
  const resources = [
146
144
  ...appResources,
@@ -190,6 +188,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
190
188
  const assistant = assistantSettings(options.assistant, globalThis.process?.env ?? {});
191
189
  const runtime = runtimeUrl(options.runtime, globalThis.process?.env ?? {});
192
190
  const firstAdmin = firstAdminUrl(globalThis.process?.env ?? {});
191
+ const runsOnNote = readRunsOnNote(globalThis.process?.env ?? {});
193
192
  const errorReporting = errorReportingSettings(options.errorReporting, globalThis.process?.env ?? {});
194
193
  const maps = mapSettings(options.maps, globalThis.process?.env ?? {});
195
194
  const senders = notifications ? notificationSenders(notifications) : [];
@@ -241,6 +240,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
241
240
  guard: createScanGuard(db, options.scanGuard),
242
241
  hooks: [
243
242
  ...options.writeHooks ?? [],
243
+ ...served.hooks,
244
244
  ...table ? [subscriptionWriteCheck(registry.entries.map((entry) => entry.model))] : [],
245
245
  ...fill ? [fillHook(() => deps, fill)] : []
246
246
  ],
@@ -289,7 +289,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
289
289
  }) }
290
290
  };
291
291
  const appName = shownAppName(auth, globalThis.process?.env ?? {});
292
- const meta = createMeta(deps, appName, icons?.favicon ? faviconPath : void 0);
292
+ const appIcon = icons?.favicon ? faviconPath : void 0;
293
+ const meta = createMeta(deps, appName, appIcon);
293
294
  const onError = createErrorHandler(options.onUnhandledError, deps.statementTimeoutMs);
294
295
  const guarded = (routes, caller = authenticate) => {
295
296
  const app = new Hono();
@@ -310,7 +311,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
310
311
  app.route("/", recordRoutes(deps));
311
312
  }, withAnonymous(authenticate, options.roles));
312
313
  const system = guarded((app) => {
313
- app.route("/", docsRoutes(deps));
314
+ app.route("/", docsRoutes(deps, appName));
314
315
  });
315
316
  const metaApp = guarded((app) => {
316
317
  app.route("/", metaRoutes(meta));
@@ -339,7 +340,10 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
339
340
  anonymous: visitorsBrowse(options.roles),
340
341
  plugins: pluginsBeforeSignIn(plugins),
341
342
  ...locale && { locale },
342
- ...firstAdmin && { firstAdminUrl: firstAdmin }
343
+ ...firstAdmin && { firstAdminUrl: firstAdmin },
344
+ appName,
345
+ ...appIcon && { appIcon },
346
+ runsOnNote
343
347
  }));
344
348
  if (auth.organizations) app.route(authBasePath, organizationRoutes({
345
349
  auth,
package/dist/index.d.ts CHANGED
@@ -102,6 +102,7 @@ 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
104
  import { checkUserTablesSchema, userTablesMigration } from "./user-tables/schema.js";
105
+ import { ownedDefinitionsMigration } from "./user-tables/owned-definitions.js";
105
106
  import { UserTables, userTableResources } from "./user-tables/user-table-resources.js";
106
107
  import { readDefinitionsVersion } from "./user-tables/definitions-version.js";
107
108
  import { reloadingAdmin } from "./user-tables/reloading-admin.js";
@@ -188,4 +189,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
188
189
  import { jobsDrainApp } from "./jobs/drain-route.js";
189
190
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
190
191
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
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 };
192
+ 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, ownedDefinitionsMigration, 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
@@ -114,6 +114,7 @@ import { checkResourcesOutsideAuthStore } from "./better-auth/store/resources-ou
114
114
  import { checkComputed, computedMigration } from "./computed/computed-schema.js";
115
115
  import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
116
116
  import { checkUserTablesSchema, userTablesMigration } from "./user-tables/schema.js";
117
+ import { ownedDefinitionsMigration } from "./user-tables/owned-definitions.js";
117
118
  import { userTableResources } from "./user-tables/user-table-resources.js";
118
119
  import { readDefinitionsVersion } from "./user-tables/definitions-version.js";
119
120
  import { reloadingAdmin } from "./user-tables/reloading-admin.js";
@@ -139,4 +140,4 @@ import { defineEmail } from "./mail/define-email.js";
139
140
  import { definePdf } from "./pdf/define-pdf.js";
140
141
  import { startJobs } from "./jobs/runtime.js";
141
142
  import { jobsDrainApp } from "./jobs/drain-route.js";
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 };
143
+ 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, ownedDefinitionsMigration, 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 };
@@ -4,11 +4,13 @@ import { resourceSchemas } from "./schemas.js";
4
4
  //#region src/openapi/document.ts
5
5
  const resourcesServer = (basePath) => basePath.slice(basePath.lastIndexOf("/") + 1);
6
6
  const systemServer = ".";
7
+ /** The title of the app's API reference and OpenAPI document, such as "Shop & Co API". */
8
+ const apiTitle = (appName) => `${appName} API`;
7
9
  /** The OpenAPI 3.1 document for one caller, generated from the resource models they may see and their zod record schemas. */
8
- const openApiDocument = (accesses, basePath) => ({
10
+ const openApiDocument = (accesses, basePath, appName) => ({
9
11
  openapi: "3.1.0",
10
12
  info: {
11
- title: "Protobase API",
13
+ title: apiTitle(appName),
12
14
  version: "1.0.0",
13
15
  description: "Admin API for an existing Postgres database. Follows Google's API design conventions ([AIP](https://google.aip.dev)) and reports errors as RFC 9457 `application/problem+json`."
14
16
  },
@@ -73,4 +75,4 @@ const openApiDocument = (accesses, basePath) => ({
73
75
  }
74
76
  });
75
77
  //#endregion
76
- export { openApiDocument };
78
+ export { apiTitle, openApiDocument };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.67.2";
2
+ var version = "0.67.4";
3
3
  //#endregion
4
4
  export { version };
@@ -1,18 +1,21 @@
1
1
  import { requestAccess } from "../request-access.js";
2
- import { openApiDocument } from "../openapi/document.js";
2
+ import { apiTitle, openApiDocument } from "../openapi/document.js";
3
3
  import { Hono } from "hono";
4
4
  import { Scalar } from "@scalar/hono-api-reference";
5
5
  //#region src/routes/docs.ts
6
- /** The OpenAPI document and the Scalar reference. Both need a token: the document lists what the caller may see and do. */
7
- const docsRoutes = (deps) => {
6
+ /**
7
+ * The OpenAPI document and the Scalar reference, both titled with the app's name. Both need a token: the document lists
8
+ * what the caller may see and do.
9
+ */
10
+ const docsRoutes = (deps, appName) => {
8
11
  const app = new Hono();
9
12
  app.get("/openapi.json", async (c) => {
10
13
  const visible = (await Promise.all(deps.registry.entries.map((entry) => requestAccess(deps, entry, c.get("session"))))).filter((access) => Object.values(access.resolved.operations).some(Boolean));
11
- return c.json(openApiDocument(visible, deps.basePath));
14
+ return c.json(openApiDocument(visible, deps.basePath, appName));
12
15
  });
13
16
  app.get("/docs", Scalar({
14
17
  url: "openapi.json",
15
- pageTitle: "Protobase API"
18
+ pageTitle: apiTitle(appName)
16
19
  }));
17
20
  return app;
18
21
  };
@@ -0,0 +1,15 @@
1
+ //#region src/runs-on-note.ts
2
+ const runsOnNoteVariable = "PROTOBASE_RUNS_ON_NOTE";
3
+ /**
4
+ * Whether the sign-in page ends on the "Runs on Protobase" note: the host turns it on with `PROTOBASE_RUNS_ON_NOTE=true`,
5
+ * so an app's repository cannot turn it off. Unset or empty, there is no note; any value but `true` or `false` stops the
6
+ * server at startup.
7
+ */
8
+ const readRunsOnNote = (env) => {
9
+ const value = env[runsOnNoteVariable];
10
+ if (!value) return false;
11
+ if (value !== "true" && value !== "false") throw new Error(`${runsOnNoteVariable} must be true or false, not "${value}"`);
12
+ return value === "true";
13
+ };
14
+ //#endregion
15
+ export { readRunsOnNote, runsOnNoteVariable };
@@ -0,0 +1,38 @@
1
+ import { definitionsResources, fieldsResourceName, tablesResourceName } from "./definitions-resources.js";
2
+ import { definitionsHook } from "./definitions-hook.js";
3
+ import { servedUserTables } from "./served-user-tables.js";
4
+ //#region src/user-tables/admin-user-tables.ts
5
+ /**
6
+ * What the tables users add bring to the admin, with `userTables` on: the resources over their definitions, through
7
+ * which they are created, each user table a name in code (`codeNames`, the app's and Protobase's) does not take, and the
8
+ * hook that creates their tables and columns. Without `tables`, nothing.
9
+ */
10
+ const adminUserTables = (tables, codeNames, roles) => {
11
+ if (!tables) return {
12
+ resources: [],
13
+ views: [],
14
+ hooks: [],
15
+ warnings: []
16
+ };
17
+ const definitions = definitionsResources({
18
+ roles,
19
+ taken: codeNames
20
+ });
21
+ const served = servedUserTables(tables, [
22
+ ...codeNames,
23
+ tablesResourceName,
24
+ fieldsResourceName
25
+ ]);
26
+ return {
27
+ resources: [
28
+ definitions.tables,
29
+ definitions.fields,
30
+ ...served.resources
31
+ ],
32
+ views: served.views,
33
+ hooks: [definitionsHook(codeNames)],
34
+ warnings: served.warnings
35
+ };
36
+ };
37
+ //#endregion
38
+ export { adminUserTables };
@@ -1,6 +1,8 @@
1
1
  import { CheckedDefinition } from "./check-definition.js";
2
2
  import { TableIndexes } from "@protobase/query";
3
3
  //#region src/user-tables/definition-resource.d.ts
4
+ /** The most characters a text field takes: four bytes each at most, it always fits the entry of its btree index. */
5
+ export declare const maxTextLength = 500;
4
6
  /**
5
7
  * The resource and view of a checked definition over `user_tables.<name>`, with the indexes its table has (none before
6
8
  * they are read): built with the schema builders, so it is checked, accessed and guarded as a resource of the config.
@@ -3,8 +3,10 @@ import { f, resource, view } from "@protobase/schema";
3
3
  import { indexServes } from "@protobase/query";
4
4
  //#region src/user-tables/definition-resource.ts
5
5
  const admins = ({ user }) => user?.roles?.includes("admin") ?? false;
6
+ /** The most characters a text field takes: four bytes each at most, it always fits the entry of its btree index. */
7
+ const maxTextLength = 500;
6
8
  const builders = {
7
- text: () => f.text(),
9
+ text: () => f.text().max(500),
8
10
  long_text: () => f.text(),
9
11
  integer: () => f.integer(),
10
12
  decimal: (field) => f.decimal({
@@ -59,4 +61,4 @@ const definitionResource = (definition, indexes = {
59
61
  };
60
62
  };
61
63
  //#endregion
62
- export { definitionResource };
64
+ export { definitionResource, maxTextLength };
@@ -0,0 +1,80 @@
1
+ import { badRequest } from "../problem.js";
2
+ import { definitionsSchema, userTablesSchema } from "./names.js";
3
+ import { checkDefinition } from "./check-definition.js";
4
+ import { definitionResource } from "./definition-resource.js";
5
+ import "./definitions-resources.js";
6
+ import { readDefinitions, readUserTableColumns } from "./read-definitions.js";
7
+ import { createTableSql, fieldSql, trigramIndexSql } from "./table-ddl.js";
8
+ import { indexServes, tableIndexes } from "@protobase/query";
9
+ import { sql } from "kysely";
10
+ //#region src/user-tables/definitions-hook.ts
11
+ const refuse = (field, message) => badRequest("invalid-record", `The request body is invalid: ${field}: ${message}`, { errors: [{
12
+ field,
13
+ code: "invalid-value",
14
+ message
15
+ }] });
16
+ const tablesTable = sql.table(`${definitionsSchema}.tables`);
17
+ const raiseVersion = (trx, tableId) => sql`update ${tablesTable} set version = version + 1 where id = ${tableId}`.execute(trx);
18
+ const lockedTable = async (trx, tableId) => {
19
+ const { rows } = await sql`select name, title_field from ${tablesTable} where id = ${tableId} for update`.execute(trx);
20
+ return rows[0];
21
+ };
22
+ const builtDefinition = async (trx, tableId) => {
23
+ const [definitions, columns] = await Promise.all([readDefinitions(trx), readUserTableColumns(trx)]);
24
+ const stored = definitions.find((definition) => definition.id === String(tableId));
25
+ const checked = checkDefinition(stored, columns);
26
+ if ("problem" in checked) throw new Error(`A definition the API wrote does not check: ${checked.problem}`);
27
+ const indexes = await tableIndexes(trx, definitionResource(checked.definition).resource.toModel());
28
+ definitionResource(checked.definition, indexes);
29
+ return {
30
+ definition: checked.definition,
31
+ indexes
32
+ };
33
+ };
34
+ const createTable = async (trx, table) => {
35
+ if (table.title_field != null) throw refuse("title_field", "Set the title field once its field is added");
36
+ const { rows } = await sql`select to_regclass(format('%I.%I', ${userTablesSchema}::text, ${table.name}::text)) is not null as found`.execute(trx);
37
+ if (rows[0].found) throw refuse("name", `The database already has a table "${table.name}" for the tables users add`);
38
+ await createTableSql(table.name).execute(trx);
39
+ await builtDefinition(trx, table.id);
40
+ };
41
+ const addField = async (trx, field, taken) => {
42
+ const table = await lockedTable(trx, field.table_id);
43
+ if (taken.includes(table.name)) throw refuse("table_id", `"${table.name}" is a resource in the app's code now, which owns its fields`);
44
+ const { rows } = await sql`select exists (select from pg_attribute a join pg_class c on c.oid = a.attrelid join pg_namespace n on n.oid = c.relnamespace
45
+ where n.nspname = ${userTablesSchema} and c.relname = ${table.name} and a.attname = ${field.name} and a.attnum > 0 and not a.attisdropped) as found`.execute(trx);
46
+ if (rows[0].found) throw refuse("name", `The table already has a column "${field.name}"`);
47
+ for (const statement of fieldSql(table.name, field)) await statement.execute(trx);
48
+ await raiseVersion(trx, field.table_id);
49
+ await builtDefinition(trx, field.table_id);
50
+ };
51
+ const setTitle = async (trx, table) => {
52
+ const title = table.title_field;
53
+ const { rows } = await sql`select type from ${sql.table(`${definitionsSchema}.fields`)} where table_id = ${table.id} and name = ${title}`.execute(trx);
54
+ if (rows[0]?.type !== "text" && rows[0]?.type !== "long_text") throw refuse("title_field", `"${title}" is not a text field of this table`);
55
+ const { definition, indexes } = await builtDefinition(trx, table.id);
56
+ if (indexes.indexes.some((index) => indexServes(index, title, "trigram", /* @__PURE__ */ new Set()))) return;
57
+ await trigramIndexSql(definition.name, title).execute(trx);
58
+ };
59
+ /**
60
+ * Creates in the database what a write of a definition describes, inside its transaction, from the rows as written: a
61
+ * new table, a new field's column and index, the title's search index. Changing a field raises its table's version, so
62
+ * every process serves the change; any update of a table, a label or its title, raises it already, as the version column
63
+ * of the record's ETag (`etagBump` in `records.ts`). A definition is never deleted.
64
+ * `taken` holds the names of the app's resources and those Protobase adds.
65
+ */
66
+ const definitionsHook = (taken) => async (event, trx) => {
67
+ const { name } = event.resource;
68
+ if (name !== "user_tables" && name !== "user_table_fields") return;
69
+ if (event.operation === "delete" || event.operation === "undelete") throw new Error(`A ${name} record is never deleted`);
70
+ const after = event.after;
71
+ if (name === "user_tables") {
72
+ if (event.operation === "create") return createTable(trx, after);
73
+ if (after.title_field != null && after.title_field !== event.before?.title_field) return setTitle(trx, after);
74
+ return;
75
+ }
76
+ if (event.operation === "create") return addField(trx, after, taken);
77
+ await raiseVersion(trx, after.table_id);
78
+ };
79
+ //#endregion
80
+ export { definitionsHook };
@@ -0,0 +1,54 @@
1
+ import { definitionFieldTypes, definitionsSchema, nameProblem } from "./names.js";
2
+ import { f, resource } from "@protobase/schema";
3
+ //#region src/user-tables/definitions-resources.ts
4
+ /** The names of the resources over the definitions: a table users add takes neither. */
5
+ const tablesResourceName = "user_tables";
6
+ const fieldsResourceName = "user_table_fields";
7
+ const admins = ({ user }) => user?.roles?.includes("admin") ?? false;
8
+ const never = () => false;
9
+ const createOnly = { update: never };
10
+ const accessOf = (roles) => roles ? { delete: never } : {
11
+ list: admins,
12
+ read: admins,
13
+ create: admins,
14
+ update: admins,
15
+ delete: never
16
+ };
17
+ const issue = (field, problem) => problem ? [{
18
+ field,
19
+ message: problem
20
+ }] : [];
21
+ /**
22
+ * The definitions of the tables users add, as resources: a table at `/user_tables/<id>`, each of its fields at
23
+ * `/user_table_fields/<table_id>,<name>`. Writing one creates its table or column in the database, in the same
24
+ * transaction (`definitionsHook`). `taken` holds the names of the app's resources and those Protobase adds, which a
25
+ * new table may not have.
26
+ */
27
+ const definitionsResources = ({ roles, taken }) => {
28
+ return {
29
+ tables: resource(tablesResourceName).table(`${definitionsSchema}.tables`).fields({
30
+ id: f.bigint().readOnly().dbDefault().filterable().sortable(),
31
+ name: f.text().access(createOnly).filterable().sortable(),
32
+ singular_label: f.text().max(200),
33
+ plural_label: f.text().max(200),
34
+ title_field: f.text().optional(),
35
+ version: f.bigint().readOnly().dbDefault()
36
+ }).primaryKey((r) => r.id).explicitGrants().access(accessOf(roles)).validate((table) => [...issue("name", nameProblem(table.name, "table") ?? (taken.includes(table.name) || ["user_tables", "user_table_fields"].includes(table.name) ? `"${table.name}" is the name of a resource of the app` : void 0)), ...issue("title_field", table.title_field ? nameProblem(table.title_field, "field") : void 0)]),
37
+ fields: resource(fieldsResourceName).table(`${definitionsSchema}.fields`).fields({
38
+ table_id: f.relation(tablesResourceName).access(createOnly).filterable(),
39
+ name: f.text().access(createOnly),
40
+ label: f.text().max(200),
41
+ type: f.enum(definitionFieldTypes).access(createOnly),
42
+ required: f.boolean().default(false).access(createOnly),
43
+ position: f.integer().sortable(),
44
+ precision: f.integer().optional().access(createOnly),
45
+ scale: f.integer().optional().access(createOnly)
46
+ }).primaryKey((r) => [r.table_id, r.name]).explicitGrants().access(accessOf(roles)).validate((field) => [
47
+ ...issue("name", nameProblem(field.name, "field")),
48
+ ...issue("required", field.required ? "A field users add is optional: the records already there have no value for it" : void 0),
49
+ ...field.type === "decimal" ? [...issue("precision", field.precision == null || field.precision < 1 || field.precision > 1e3 ? "A decimal needs a precision from 1 to 1000" : void 0), ...issue("scale", field.scale == null || field.scale < 0 || field.precision != null && field.scale > field.precision ? "A decimal needs a scale from 0 to its precision" : void 0)] : [...issue("precision", field.precision == null ? void 0 : "Only a decimal has a precision"), ...issue("scale", field.scale == null ? void 0 : "Only a decimal has a scale")]
50
+ ])
51
+ };
52
+ };
53
+ //#endregion
54
+ export { definitionsResources, fieldsResourceName, tablesResourceName };
@@ -1,3 +1,4 @@
1
+ import { reservedWords } from "./reserved-words.js";
1
2
  //#region src/user-tables/names.ts
2
3
  /** The schema the definitions of the tables users add live in: `tables`, and `fields` of each. */
3
4
  const definitionsSchema = "table_definitions";
@@ -18,5 +19,13 @@ const definitionColumnTypes = {
18
19
  };
19
20
  const definitionFieldTypes = Object.keys(definitionColumnTypes);
20
21
  const isUserTableName = (name) => userTableName.test(name) && new TextEncoder().encode(name).length <= 63;
22
+ /** The names a field never takes: the key's, and the API's for the record version. */
23
+ const reservedFieldNames = ["id", "etag"];
24
+ /** What is wrong with a name a table or field users add is given, or `undefined` when it may have it. */
25
+ const nameProblem = (name, kind) => {
26
+ if (typeof name !== "string" || !isUserTableName(name)) return "Use lowercase letters, digits and \"_\", starting with a letter, at most 63 bytes";
27
+ if (reservedWords.has(name)) return `"${name}" is a word SQL reserves`;
28
+ if (kind === "field" && reservedFieldNames.includes(name)) return `"${name}" is the name of a field every table has`;
29
+ };
21
30
  //#endregion
22
- export { definitionColumnTypes, definitionFieldTypes, definitionsSchema, isUserTableName, userTableName, userTablesSchema };
31
+ export { definitionColumnTypes, definitionFieldTypes, definitionsSchema, isUserTableName, nameProblem, reservedFieldNames, userTableName, userTablesSchema };
@@ -0,0 +1,12 @@
1
+ import { ResourceModel } from "@protobase/schema";
2
+ import { Db } from "@protobase/query";
3
+ //#region src/user-tables/owned-definitions.d.ts
4
+ /** The user tables a resource in code owns, as `protobase eject` writes one: over `user_tables.<name>`, with that name. */
5
+ export declare const ownedTableNames: (models: readonly ResourceModel[]) => string[];
6
+ /**
7
+ * The delete of the definitions of the user tables a resource in code owns, with their fields; their tables and rows
8
+ * stay. `undefined` when no such definition is left. `protobase migration` writes it once the code that owns them has
9
+ * deployed, so a definition goes in a later deploy than the one that stopped reading it.
10
+ */
11
+ export declare const ownedDefinitionsMigration: (db: Db, models: readonly ResourceModel[]) => Promise<string | undefined>;
12
+ //#endregion
@@ -0,0 +1,22 @@
1
+ import { definitionsSchema } from "./names.js";
2
+ import { sql } from "kysely";
3
+ //#region src/user-tables/owned-definitions.ts
4
+ /** The user tables a resource in code owns, as `protobase eject` writes one: over `user_tables.<name>`, with that name. */
5
+ const ownedTableNames = (models) => models.filter((model) => model.table.schema === "user_tables" && model.table.name === model.name).map((model) => model.name);
6
+ const literal = (text) => `'${text.replaceAll("'", "''")}'`;
7
+ /**
8
+ * The delete of the definitions of the user tables a resource in code owns, with their fields; their tables and rows
9
+ * stay. `undefined` when no such definition is left. `protobase migration` writes it once the code that owns them has
10
+ * deployed, so a definition goes in a later deploy than the one that stopped reading it.
11
+ */
12
+ const ownedDefinitionsMigration = async (db, models) => {
13
+ const owned = ownedTableNames(models);
14
+ if (owned.length === 0) return;
15
+ const { rows: found } = await sql`select to_regclass(${`${definitionsSchema}.tables`}) is not null as found`.execute(db);
16
+ if (!found[0].found) return;
17
+ const { rows } = await sql`select name from ${sql.table(`${definitionsSchema}.tables`)} where name = any(${owned}) order by name`.execute(db);
18
+ if (rows.length === 0) return;
19
+ return `delete from ${definitionsSchema}.tables where name in (${rows.map((row) => literal(row.name)).join(", ")});`;
20
+ };
21
+ //#endregion
22
+ export { ownedDefinitionsMigration, ownedTableNames };
@@ -27,8 +27,9 @@ export type ReloadingAdminInput = {
27
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
28
  * the admin built anew. A request finishes on the admin it started with, and a WebSocket keeps the admin it opened
29
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`.
30
+ * A write to a definition that succeeded rebuilds it at once, before it is answered, so the next request of the writer
31
+ * finds the change. `reload()` reads the version at once, for the process that changed a definition another way. The
32
+ * jobs, the realtime prune and the field decrypter are those of `first`.
32
33
  */
33
34
  export declare const reloadingAdmin: ({ first, version, readVersion, readTables, build, report, statementTimeoutMs, now }: ReloadingAdminInput) => Hono<AdminEnv, import("hono/types").BlankSchema, "/"> & {
34
35
  jobs: JobDefinition[];
@@ -1,4 +1,5 @@
1
1
  import { createErrorHandler } from "../error-handler.js";
2
+ import "./definitions-resources.js";
2
3
  import { latestAdmin } from "./latest-admin.js";
3
4
  import { Hono } from "hono";
4
5
  //#region src/user-tables/reloading-admin.ts
@@ -10,14 +11,16 @@ const replayable = async (request) => {
10
11
  const body = await request.arrayBuffer();
11
12
  return () => new Request(request, { body });
12
13
  };
14
+ const writesDefinitions = (request, path) => request.method !== "GET" && request.method !== "HEAD" && (path.endsWith(":batchWrite") || path.split("/").some((segment) => segment === "user_tables" || segment === "user_table_fields"));
13
15
  /**
14
16
  * The app's admin, rebuilt when the tables users add change, so every process serves a new definition without a
15
17
  * restart. `/meta` reads the definitions version first; any other request at most once a second, and once more when the
16
18
  * 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
19
  * the admin built anew. A request finishes on the admin it started with, and a WebSocket keeps the admin it opened
18
20
  * 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
+ * A write to a definition that succeeded rebuilds it at once, before it is answered, so the next request of the writer
22
+ * finds the change. `reload()` reads the version at once, for the process that changed a definition another way. The
23
+ * jobs, the realtime prune and the field decrypter are those of `first`.
21
24
  */
22
25
  const reloadingAdmin = ({ first, version, readVersion, readTables, build, report, statementTimeoutMs, now }) => {
23
26
  const rebuild = async (current) => {
@@ -36,8 +39,7 @@ const reloadingAdmin = ({ first, version, readVersion, readTables, build, report
36
39
  rebuild,
37
40
  ...now && { now }
38
41
  });
39
- const answer = async (request, env) => {
40
- const path = new URL(request.url).pathname;
42
+ const answerOn = async (request, env, path) => {
41
43
  const atOnce = path === first.metaPath || path.startsWith(`${first.metaPath}/`);
42
44
  const admin = await latest.get(atOnce);
43
45
  const again = atOnce ? void 0 : await replayable(request);
@@ -47,6 +49,12 @@ const reloadingAdmin = ({ first, version, readVersion, readTables, build, report
47
49
  const fresh = await latest.get(true);
48
50
  return fresh === admin ? answered : fresh.fetch(again(), env);
49
51
  };
52
+ const answer = async (request, env) => {
53
+ const path = new URL(request.url).pathname;
54
+ const answered = await answerOn(request, env, path);
55
+ if (answered.ok && writesDefinitions(request, path)) await latest.get(true);
56
+ return answered;
57
+ };
50
58
  const app = new Hono();
51
59
  app.onError(createErrorHandler(report, statementTimeoutMs));
52
60
  app.all("*", (c) => answer(c.req.raw, c.env));
@@ -0,0 +1,87 @@
1
+ //#region src/user-tables/reserved-words.ts
2
+ /**
3
+ * The key words Postgres 18 reserves, `select word from pg_get_keywords() where catcode = 'R'`: a table or field users
4
+ * add never takes one, so its name works unquoted in any SQL a developer writes against it.
5
+ */
6
+ const reservedWords = /* @__PURE__ */ new Set([
7
+ "all",
8
+ "analyse",
9
+ "analyze",
10
+ "and",
11
+ "any",
12
+ "array",
13
+ "as",
14
+ "asc",
15
+ "asymmetric",
16
+ "both",
17
+ "case",
18
+ "cast",
19
+ "check",
20
+ "collate",
21
+ "column",
22
+ "constraint",
23
+ "create",
24
+ "current_catalog",
25
+ "current_date",
26
+ "current_role",
27
+ "current_time",
28
+ "current_timestamp",
29
+ "current_user",
30
+ "default",
31
+ "deferrable",
32
+ "desc",
33
+ "distinct",
34
+ "do",
35
+ "else",
36
+ "end",
37
+ "except",
38
+ "false",
39
+ "fetch",
40
+ "for",
41
+ "foreign",
42
+ "from",
43
+ "grant",
44
+ "group",
45
+ "having",
46
+ "in",
47
+ "initially",
48
+ "intersect",
49
+ "into",
50
+ "lateral",
51
+ "leading",
52
+ "limit",
53
+ "localtime",
54
+ "localtimestamp",
55
+ "not",
56
+ "null",
57
+ "offset",
58
+ "on",
59
+ "only",
60
+ "or",
61
+ "order",
62
+ "placing",
63
+ "primary",
64
+ "references",
65
+ "returning",
66
+ "select",
67
+ "session_user",
68
+ "some",
69
+ "symmetric",
70
+ "system_user",
71
+ "table",
72
+ "then",
73
+ "to",
74
+ "trailing",
75
+ "true",
76
+ "union",
77
+ "unique",
78
+ "user",
79
+ "using",
80
+ "variadic",
81
+ "when",
82
+ "where",
83
+ "window",
84
+ "with"
85
+ ]);
86
+ //#endregion
87
+ export { reservedWords };
@@ -1,15 +1,17 @@
1
1
  import { definitionFieldTypes, definitionsSchema, userTableName, userTablesSchema } from "./names.js";
2
2
  //#region src/user-tables/schema-sql.ts
3
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. */
4
+ /** The table of definitions: one row per table users add, its labels and a version, 1 when added, that each change of it raises. */
5
5
  const tablesSql = () => `create table ${definitionsSchema}.tables (
6
6
  id bigint generated always as identity primary key,
7
7
  name text not null unique ${nameCheck("name")},
8
8
  singular_label text not null,
9
9
  plural_label text not null,
10
10
  title_field text ${nameCheck("title_field")},
11
- version bigint not null check (version > 0)
11
+ version bigint not null default 1 check (version > 0)
12
12
  )`;
13
+ /** Gives a definitions table made before new definitions started at version 1 that default. */
14
+ const versionDefaultSql = () => `alter table ${definitionsSchema}.tables alter column version set default 1`;
13
15
  /** The fields of each definition, in order, each with its label and type; a decimal names its precision and scale. */
14
16
  const fieldsSql = () => `create table ${definitionsSchema}.fields (
15
17
  table_id bigint not null references ${definitionsSchema}.tables (id) on delete cascade,
@@ -32,4 +34,4 @@ const schemasSql = () => [
32
34
  `grant usage, create on schema ${userTablesSchema} to current_user`
33
35
  ];
34
36
  //#endregion
35
- export { fieldsSql, schemasSql, tablesSql };
37
+ export { fieldsSql, schemasSql, tablesSql, versionDefaultSql };
@@ -1,11 +1,12 @@
1
1
  import { migrationCommand } from "../changes/capture-schema.js";
2
2
  import { definitionsSchema, userTablesSchema } from "./names.js";
3
- import { fieldsSql, schemasSql, tablesSql } from "./schema-sql.js";
3
+ import { fieldsSql, schemasSql, tablesSql, versionDefaultSql } from "./schema-sql.js";
4
4
  import { sql } from "kysely";
5
5
  //#region src/user-tables/schema.ts
6
6
  const readUserTablesSchema = async (db) => {
7
7
  const { rows } = await sql`select
8
8
  to_regclass(${`${definitionsSchema}.tables`}) is not null as tables,
9
+ exists (select from information_schema.columns where table_schema = ${definitionsSchema} and table_name = 'tables' and column_name = 'version' and column_default is not null) as "versionDefault",
9
10
  to_regclass(${`${definitionsSchema}.fields`}) is not null as fields,
10
11
  exists (select from pg_namespace where nspname = ${userTablesSchema}) as data,
11
12
  exists (select from pg_extension where extname = 'pg_trgm') as trgm`.execute(db);
@@ -17,18 +18,18 @@ const readUserTablesSchema = async (db) => {
17
18
  */
18
19
  const userTablesMigration = async (db) => {
19
20
  const found = await readUserTablesSchema(db);
20
- if (found.tables && found.fields && found.data && found.trgm) return void 0;
21
+ if (found.tables && found.versionDefault && found.fields && found.data && found.trgm) return void 0;
21
22
  return `${[
22
23
  ...schemasSql(),
23
- ...found.tables ? [] : [tablesSql()],
24
+ ...found.tables ? found.versionDefault ? [] : [versionDefaultSql()] : [tablesSql()],
24
25
  ...found.fields ? [] : [fieldsSql()]
25
26
  ].join(";\n\n")};`;
26
27
  };
27
28
  /** Refuses to go on while the schemas of user tables are missing; the project's migrations add them. */
28
29
  const checkUserTablesSchema = async (db) => {
29
30
  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.`);
31
+ if (found.tables && found.versionDefault && found.fields && found.data && found.trgm) return;
32
+ throw new Error(`The ${definitionsSchema} or ${userTablesSchema} schema, the default of a definition's version, 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
  };
33
34
  //#endregion
34
35
  export { checkUserTablesSchema, userTablesMigration };
@@ -0,0 +1,56 @@
1
+ import { nameProblem, userTablesSchema } from "./names.js";
2
+ import { sql } from "kysely";
3
+ //#region src/user-tables/table-ddl.ts
4
+ const checkedName = (name, kind) => {
5
+ const problem = nameProblem(name, kind);
6
+ if (problem) throw new Error(`The ${kind} name ${JSON.stringify(name)} cannot reach SQL: ${problem}`);
7
+ return name;
8
+ };
9
+ const tableId = (table) => sql.id(userTablesSchema, checkedName(table, "table"));
10
+ const decimalOf = (precision, scale) => {
11
+ if (!Number.isInteger(precision) || precision < 1 || precision > 1e3) throw new Error(`The precision ${JSON.stringify(precision)} is not a whole number from 1 to 1000`);
12
+ if (!Number.isInteger(scale) || scale < 0 || scale > precision) throw new Error(`The scale ${JSON.stringify(scale)} is not a whole number from 0 to the precision`);
13
+ return sql.raw(`numeric(${precision}, ${scale})`);
14
+ };
15
+ const columnType = (field) => {
16
+ switch (field.type) {
17
+ case "text":
18
+ case "long_text": return sql.raw("text");
19
+ case "integer": return sql.raw("integer");
20
+ case "decimal": return decimalOf(field.precision, field.scale);
21
+ case "boolean": return sql.raw("boolean");
22
+ case "date": return sql.raw("date");
23
+ case "timestamp": return sql.raw("timestamptz");
24
+ case "json": return sql.raw("jsonb");
25
+ default: throw new Error(`The field type ${JSON.stringify(field.type)} has no column type`);
26
+ }
27
+ };
28
+ /** The index a field's column gets so its list filters and sorts: btree, a trigram one for long text, which outgrows a btree entry, and none for json. */
29
+ const indexKind = (type) => {
30
+ switch (type) {
31
+ case "text":
32
+ case "integer":
33
+ case "decimal":
34
+ case "boolean":
35
+ case "date":
36
+ case "timestamp": return "btree";
37
+ case "long_text": return "trigram";
38
+ case "json": return;
39
+ default: throw new Error(`The field type ${JSON.stringify(type)} has no index kind`);
40
+ }
41
+ };
42
+ /** The new table, with its key alone: its fields are added as columns. */
43
+ const createTableSql = (table) => sql`create table ${tableId(table)} (id bigint generated always as identity primary key)`;
44
+ /** The field's column: nullable and without a default, so the records already there read it as empty. */
45
+ const addColumnSql = (table, field) => sql`alter table ${tableId(table)} add column ${sql.id(checkedName(field.name, "field"))} ${columnType(field)}`;
46
+ const btreeSql = (table, column) => sql`create index on ${tableId(table)} (${sql.id(checkedName(column, "field"))})`;
47
+ /** The pg_trgm index that search and text filters need on a text column. */
48
+ const trigramIndexSql = (table, column) => sql`create index on ${tableId(table)} using gin (${sql.id(checkedName(column, "field"))} gin_trgm_ops)`;
49
+ /** The column and the index of a field added to a table. */
50
+ const fieldSql = (table, field) => {
51
+ const kind = indexKind(field.type);
52
+ const index = kind === "btree" ? [btreeSql(table, field.name)] : kind === "trigram" ? [trigramIndexSql(table, field.name)] : [];
53
+ return [addColumnSql(table, field), ...index];
54
+ };
55
+ //#endregion
56
+ export { addColumnSql, createTableSql, fieldSql, indexKind, trigramIndexSql };
@@ -1,10 +1,15 @@
1
+ import { CheckedDefinition } from "./check-definition.js";
1
2
  import { definitionResource } from "./definition-resource.js";
2
3
  import { Db } from "@protobase/query";
3
4
  //#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
+ /**
6
+ * What the stored definitions serve: a resource and view for each that fits the database, the definitions they are
7
+ * built from, as `protobase eject` writes them into code, and why each other one is left out.
8
+ */
5
9
  export type UserTables = {
6
10
  resources: Array<ReturnType<typeof definitionResource>['resource']>;
7
11
  views: Array<ReturnType<typeof definitionResource>['view']>;
12
+ definitions: CheckedDefinition[];
8
13
  problems: string[];
9
14
  };
10
15
  /**
@@ -13,6 +13,7 @@ const userTableResources = async (db) => {
13
13
  const tables = {
14
14
  resources: [],
15
15
  views: [],
16
+ definitions: [],
16
17
  problems: []
17
18
  };
18
19
  for (const stored of definitions) {
@@ -25,6 +26,7 @@ const userTableResources = async (db) => {
25
26
  const { resource, view } = definitionResource(checked.definition, indexes);
26
27
  tables.resources.push(resource);
27
28
  tables.views.push(view);
29
+ tables.definitions.push(checked.definition);
28
30
  }
29
31
  return tables;
30
32
  };
@@ -1,7 +1,7 @@
1
1
  import { pngIcons } from "@protobase/schema/png-icons";
2
2
  //#region src/web-app/app-icons.d.ts
3
3
  export type PngIcon = keyof typeof pngIcons;
4
- /** The icons a project has of its own: `favicon` when the favicon is its own rather than Protobase's, and its PNGs. */
4
+ /** The icons a project has of its own: `favicon` when the favicon is its own rather than the neutral mark, and its PNGs. */
5
5
  export type AppIcons = {
6
6
  favicon: boolean;
7
7
  png: PngIcon[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.67.2",
3
+ "version": "0.67.4",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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.2",
47
- "@protobase/query": "0.67.2",
48
- "@protobase/schema": "0.67.2",
46
+ "@protobase/layout": "0.67.4",
47
+ "@protobase/query": "0.67.4",
48
+ "@protobase/schema": "0.67.4",
49
49
  "@react-email/render": "^2.1.0",
50
50
  "@react-pdf/primitives": "^4.4.0",
51
51
  "@react-pdf/renderer": "^4.9.0",