@protobase/server 0.21.6 → 0.21.8

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.
Files changed (91) hide show
  1. package/dist/aggregate-groups.js +1 -1
  2. package/dist/aggregate-service.js +3 -3
  3. package/dist/ai/ai-fields.js +9 -0
  4. package/dist/ai/ai-model.js +9 -0
  5. package/dist/ai/check-ai.d.ts +13 -0
  6. package/dist/ai/check-ai.js +11 -0
  7. package/dist/ai/fill-hook.js +47 -0
  8. package/dist/ai/fill-job.js +67 -0
  9. package/dist/ai/generate-route.js +57 -0
  10. package/dist/ai/generate-value.js +28 -0
  11. package/dist/ai/values.js +15 -0
  12. package/dist/anonymous.js +7 -2
  13. package/dist/assistant/assistant-settings.d.ts +2 -2
  14. package/dist/assistant/assistant-settings.js +7 -21
  15. package/dist/assistant/built-in-assistant.js +2 -1
  16. package/dist/assistant/model-settings.js +31 -0
  17. package/dist/assistant/turn.d.ts +1 -1
  18. package/dist/assistant/turn.js +2 -2
  19. package/dist/batch/reorder.js +2 -2
  20. package/dist/batch/run-batch.js +1 -1
  21. package/dist/better-auth/create-auth.d.ts +12 -4
  22. package/dist/better-auth/create-auth.js +0 -1
  23. package/dist/better-auth/organizations/invitation-endpoints.js +1 -1
  24. package/dist/better-auth/organizations/member-endpoints.js +1 -1
  25. package/dist/better-auth/passkey-plugin.js +1 -1
  26. package/dist/better-auth/policy-plugin.js +1 -1
  27. package/dist/better-auth/roles/roles-plugin.js +1 -1
  28. package/dist/better-auth/sign-up/sign-up-plugin.js +1 -1
  29. package/dist/better-auth/staff-sign-in-endpoints.js +1 -1
  30. package/dist/better-auth/status-route.js +1 -1
  31. package/dist/changes/match-events.js +1 -1
  32. package/dist/check-roles.js +1 -1
  33. package/dist/check-shell.js +1 -1
  34. package/dist/check-views.js +26 -4
  35. package/dist/comments/app-origin.js +11 -0
  36. package/dist/comments/comment-access.js +54 -0
  37. package/dist/comments/comment-input.js +26 -0
  38. package/dist/comments/comment-item.d.ts +16 -0
  39. package/dist/comments/comment-item.js +18 -0
  40. package/dist/comments/comment-users.d.ts +21 -0
  41. package/dist/comments/comment-users.js +35 -0
  42. package/dist/comments/comments-sql.d.ts +8 -0
  43. package/dist/comments/comments-sql.js +37 -0
  44. package/dist/comments/mention-email.js +37 -0
  45. package/dist/comments/mention-job.d.ts +3 -0
  46. package/dist/comments/mention-job.js +43 -0
  47. package/dist/comments/mentionable.js +38 -0
  48. package/dist/comments/post-comment.js +57 -0
  49. package/dist/comments/read-comments.js +45 -0
  50. package/dist/comments/record-comments.js +53 -0
  51. package/dist/comments/record-label.js +19 -0
  52. package/dist/create-admin.d.ts +9 -3
  53. package/dist/create-admin.js +55 -11
  54. package/dist/deps.d.ts +2 -0
  55. package/dist/error-handler.js +1 -1
  56. package/dist/files/attach.js +1 -1
  57. package/dist/files/upload.js +2 -2
  58. package/dist/forms/challenge.d.ts +8 -0
  59. package/dist/forms/challenge.js +71 -0
  60. package/dist/forms/check-forms.js +59 -0
  61. package/dist/forms/rate-limit.js +38 -0
  62. package/dist/forms/routes.js +83 -0
  63. package/dist/forms/runtime.js +31 -0
  64. package/dist/history/read-history.js +4 -3
  65. package/dist/index.d.ts +7 -3
  66. package/dist/index.js +11 -9
  67. package/dist/jobs/runtime.js +1 -1
  68. package/dist/list-photos.js +3 -3
  69. package/dist/list-service.js +2 -2
  70. package/dist/map-service.js +1 -1
  71. package/dist/meta.js +4 -4
  72. package/dist/openapi/resource-paths.js +151 -0
  73. package/dist/project-config.d.ts +1 -1
  74. package/dist/read-service.js +2 -2
  75. package/dist/related-labels.js +1 -1
  76. package/dist/request-access.js +26 -2
  77. package/dist/routes/record.js +17 -0
  78. package/dist/row-permissions.js +3 -3
  79. package/dist/sql-parts.js +1 -1
  80. package/dist/types.d.ts +2 -0
  81. package/dist/validate-body.js +6 -1
  82. package/dist/view-access.js +35 -6
  83. package/dist/view-related.js +15 -2
  84. package/dist/webhooks/route-events.js +1 -1
  85. package/dist/webhooks/table/schema.js +1 -1
  86. package/dist/webhooks/table/webhook-table.js +1 -1
  87. package/dist/webhooks/webhooks.js +1 -1
  88. package/dist/write-pipeline.js +8 -5
  89. package/dist/write-shared.js +3 -2
  90. package/dist/write-update.js +1 -1
  91. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1,17 +1,19 @@
1
+ import { HttpProblem, MissingCredentials, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
2
+ import { formatUri, variantPath } from "./files/uri.js";
3
+ import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
4
+ import { isCaptured } from "./changes/capture-sql.js";
5
+ import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
6
+ import { FatalJobError } from "./jobs/run-job.js";
7
+ import { ModelError, streamChatCompletion } from "./assistant/chat-completions.js";
1
8
  import { assistantRoles, assistantSettings, assistantVariables, seesAssistant } from "./assistant/assistant-settings.js";
2
9
  import { conversationKey, createConversations } from "./assistant/conversations.js";
3
10
  import { fileConversationStore } from "./assistant/file-conversation-store.js";
4
- import { HttpProblem, MissingCredentials, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
5
11
  import { assistantProtocolRoutes } from "./assistant/protocol-routes.js";
6
12
  import { runReadOnlyQuery, runReadWriteQuery } from "./assistant/run-query.js";
7
13
  import { defineTool } from "./assistant/tools.js";
8
14
  import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.js";
9
15
  import { requestApproval } from "./assistant/approval.js";
10
16
  import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
11
- import { ModelError, streamChatCompletion } from "./assistant/chat-completions.js";
12
- import { formatUri, variantPath } from "./files/uri.js";
13
- import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
14
- import { isCaptured } from "./changes/capture-sql.js";
15
17
  import { runTurn } from "./assistant/turn.js";
16
18
  import { consoleAuditQueue } from "./audit/console-queue.js";
17
19
  import { webauthnPath } from "./better-auth/origins.js";
@@ -22,12 +24,12 @@ import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
22
24
  import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
23
25
  import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
24
26
  import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
25
- import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
27
+ import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
28
+ import { checkComments, commentsMigration } from "./comments/comments-sql.js";
26
29
  import { coalesce } from "./changes/coalesce.js";
27
30
  import { eventMatcher } from "./changes/match-events.js";
28
31
  import { parseColumns, readOutbox } from "./changes/outbox.js";
29
32
  import { recordOf, resourceChange } from "./changes/resource-change.js";
30
- import { FatalJobError } from "./jobs/run-job.js";
31
33
  import { defineFunction } from "./functions/define-function.js";
32
34
  import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
33
35
  import { subscription } from "./webhooks/subscription.js";
@@ -51,9 +53,9 @@ import { withTrustedApps } from "./better-auth/app-tokens.js";
51
53
  import { betterAuthAuthenticator } from "./better-auth/authenticator.js";
52
54
  import { issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
53
55
  import { authSchemaMigration, checkAuthSchema } from "./better-auth/auth-schema.js";
54
- import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
55
56
  import { webhooks } from "./webhooks/table/plugin.js";
56
57
  import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
58
+ import { checkAi } from "./ai/check-ai.js";
57
59
  import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
58
60
  import { startErrorReporting } from "./error-reporting/error-reporter.js";
59
61
  import { functionApp } from "./functions/function-app.js";
@@ -66,4 +68,4 @@ import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/p
66
68
  import { defineEmail } from "./mail/define-email.js";
67
69
  import { startJobs } from "./jobs/runtime.js";
68
70
  import { jobsDrainApp } from "./jobs/drain-route.js";
69
- export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isCaptured, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
71
+ export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isCaptured, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
@@ -1,8 +1,8 @@
1
1
  import { clearCurrentJobs, setCurrentJobs } from "./current.js";
2
2
  import { jobsSchema } from "./schema.js";
3
- import { platformMailer } from "../mail/platform-mailer.js";
4
3
  import "./schedule.js";
5
4
  import { runJob, validatePayload } from "./run-job.js";
5
+ import { platformMailer } from "../mail/platform-mailer.js";
6
6
  import { jobsDatabase } from "./database.js";
7
7
  import { queueOptions, queueSettingsDiffer, sendOptions } from "./options.js";
8
8
  import { createSchedules } from "./schedules.js";
@@ -1,12 +1,12 @@
1
1
  import { badRequest } from "./problem.js";
2
- import { presentFiles } from "./files/present.js";
3
- import { exposeRow } from "./rows.js";
4
2
  import { checkedSort } from "./filter-input.js";
5
3
  import { readScope, requestAccess } from "./request-access.js";
4
+ import { presentFiles } from "./files/present.js";
5
+ import { exposeRow } from "./rows.js";
6
6
  import { readTransaction } from "./transactions.js";
7
7
  import { cardPhotosErrors } from "./view-card-photos.js";
8
- import { checkFilter, createWhere } from "@protobase/schema";
9
8
  import { cardPhotoLimit } from "@protobase/schema/internal";
9
+ import { checkFilter, createWhere } from "@protobase/schema";
10
10
  import { relatedRows } from "@protobase/query";
11
11
  //#region src/list-photos.ts
12
12
  const where = createWhere();
@@ -1,9 +1,9 @@
1
+ import { checkedFilter, checkedSort } from "./filter-input.js";
2
+ import { readScope, requestAccess, requireOperation } from "./request-access.js";
1
3
  import { presentFiles } from "./files/present.js";
2
4
  import { etagAlias } from "./sql-parts.js";
3
5
  import { etagOf } from "./etag.js";
4
6
  import { exposeRow, pickFields } from "./rows.js";
5
- import { checkedFilter, checkedSort } from "./filter-input.js";
6
- import { readScope, requestAccess, requireOperation } from "./request-access.js";
7
7
  import { rowPermissions } from "./row-permissions.js";
8
8
  import { readTransaction } from "./transactions.js";
9
9
  import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
@@ -5,8 +5,8 @@ import { parseParams, selectedFields } from "./params.js";
5
5
  import { labelRequests } from "./related-labels.js";
6
6
  import { photosRequest } from "./list-photos.js";
7
7
  import { listRecords } from "./list-service.js";
8
- import { checkMapPosition, countUpTo, mapBuckets, maxMapZoom } from "@protobase/query";
9
8
  import { z } from "zod";
9
+ import { checkMapPosition, countUpTo, mapBuckets, maxMapZoom } from "@protobase/query";
10
10
  const mapParams = z.object({
11
11
  latitude: z.string().min(1),
12
12
  longitude: z.string().min(1),
package/dist/meta.js CHANGED
@@ -1,14 +1,14 @@
1
- import { seesAssistant } from "./assistant/assistant-settings.js";
2
- import { sha256Hex, stableJson } from "./hash.js";
3
1
  import { requestAccess } from "./request-access.js";
2
+ import { sha256Hex, stableJson } from "./hash.js";
3
+ import { seesAssistant } from "./assistant/assistant-settings.js";
4
+ import { pruneView } from "./view-access.js";
4
5
  import { activeRole } from "./active-role.js";
5
6
  import { pluginsFor } from "./plugins/plugin-settings.js";
6
7
  import { seesRuntime } from "./runtime/runtime-settings.js";
7
8
  import { sourcesFor } from "./sources/source-settings.js";
8
- import { pruneView } from "./view-access.js";
9
9
  import { prunePage } from "@protobase/layout";
10
- import { pickView } from "@protobase/schema";
11
10
  import { bottomBarFor } from "@protobase/schema/internal";
11
+ import { pickView } from "@protobase/schema";
12
12
  //#region src/meta.ts
13
13
  const signInView = (view) => {
14
14
  if (!view?.signIn) return [];
@@ -1,5 +1,6 @@
1
1
  import { writableFields } from "../request-access.js";
2
2
  import { pascal } from "./naming.js";
3
+ import { pickView } from "@protobase/schema";
3
4
  import { mapCellPixels, maxMapBuckets, maxMapZoom } from "@protobase/query";
4
5
  //#region src/openapi/resource-paths.ts
5
6
  const param = (name) => ({ $ref: `#/components/parameters/${name}` });
@@ -85,6 +86,37 @@ const page = (name, seek = false) => json({
85
86
  photos
86
87
  }
87
88
  });
89
+ const person = {
90
+ type: "object",
91
+ required: ["id"],
92
+ properties: {
93
+ id: { type: "string" },
94
+ name: { type: "string" }
95
+ }
96
+ };
97
+ const comment = {
98
+ type: "object",
99
+ required: [
100
+ "id",
101
+ "created_at",
102
+ "body",
103
+ "author",
104
+ "mentions"
105
+ ],
106
+ properties: {
107
+ id: { type: "string" },
108
+ created_at: {
109
+ type: "string",
110
+ format: "date-time"
111
+ },
112
+ body: { type: "string" },
113
+ author: person,
114
+ mentions: {
115
+ type: "array",
116
+ items: person
117
+ }
118
+ }
119
+ };
88
120
  const bounds = {
89
121
  type: "object",
90
122
  properties: {
@@ -140,6 +172,7 @@ const needs = {
140
172
  create: "create",
141
173
  update: "update",
142
174
  undelete: "update",
175
+ generate: "update",
143
176
  delete: "delete"
144
177
  };
145
178
  const withoutDenied = (paths, access) => Object.fromEntries(Object.entries(paths).flatMap(([path, methods]) => {
@@ -160,6 +193,7 @@ const resourcePaths = (access) => {
160
193
  404: error("NotFound")
161
194
  };
162
195
  const sensitive = Object.values(access.readable.fields).filter((field) => field.sensitive).map((field) => field.name);
196
+ const aiActions = (pickView(access.deps.views, access.session.user.roles, entry.name)?.actions ?? []).filter((action) => action.run?.kind === "ai").map((action) => action.name);
163
197
  const paths = {
164
198
  [`/${entry.name}`]: {
165
199
  get: {
@@ -325,6 +359,31 @@ const resourcePaths = (access) => {
325
359
  ...keyErrors
326
360
  }
327
361
  } } },
362
+ ...aiActions.length > 0 && { [`/${entry.name}/{key}:generate`]: { post: {
363
+ operationId: `generate_${entry.name}`,
364
+ tags: tag,
365
+ summary: `Run an AI action on a ${entry.name} record`,
366
+ description: "The model writes the action's field from its prompt, as the caller, who must be signed in, may read the fields the prompt names and may update the field. An edit made while the model answers is a 412; empty inputs or an empty answer are a 422, and nothing is written.",
367
+ parameters: [param("key")],
368
+ requestBody: {
369
+ required: true,
370
+ content: json({
371
+ type: "object",
372
+ required: ["action"],
373
+ properties: { action: {
374
+ type: "string",
375
+ enum: aiActions
376
+ } },
377
+ additionalProperties: false
378
+ })
379
+ },
380
+ responses: {
381
+ 200: record(name, entry),
382
+ ...keyErrors,
383
+ 403: error("Forbidden"),
384
+ 412: error("PreconditionFailed")
385
+ }
386
+ } } },
328
387
  ...entry.model.history && { [`/${entry.name}/{key}:history`]: { get: {
329
388
  operationId: `history_${entry.name}`,
330
389
  tags: tag,
@@ -396,6 +455,98 @@ const resourcePaths = (access) => {
396
455
  ...keyErrors
397
456
  }
398
457
  } } },
458
+ ...entry.model.comments && {
459
+ [`/${entry.name}/{key}:comments`]: {
460
+ get: {
461
+ operationId: `comments_${entry.name}`,
462
+ tags: tag,
463
+ summary: `The comments on a ${entry.name} record`,
464
+ description: "Newest first, with the same access as a get, a deleted record included.",
465
+ parameters: [
466
+ param("key"),
467
+ query("page_size", "Comments per page, at most 100 (default 20)", { type: "integer" }),
468
+ query("page_token", "The next_page_token of the previous page")
469
+ ],
470
+ responses: {
471
+ 200: {
472
+ description: "A page of comments",
473
+ content: json({
474
+ type: "object",
475
+ required: ["items", "next_page_token"],
476
+ properties: {
477
+ items: {
478
+ type: "array",
479
+ items: comment
480
+ },
481
+ next_page_token: {
482
+ type: "string",
483
+ description: "Empty on the last page"
484
+ }
485
+ }
486
+ })
487
+ },
488
+ ...keyErrors
489
+ }
490
+ },
491
+ post: {
492
+ operationId: `comment_${entry.name}`,
493
+ tags: tag,
494
+ summary: `Comment on a ${entry.name} record`,
495
+ description: "For a signed-in caller who may read the record, not deleted. Each mentioned user must be able to read the record too, and is emailed a link to it.",
496
+ parameters: [param("key")],
497
+ requestBody: {
498
+ required: true,
499
+ content: json({
500
+ type: "object",
501
+ required: ["body"],
502
+ properties: {
503
+ body: {
504
+ type: "string",
505
+ maxLength: 1e4
506
+ },
507
+ mentions: {
508
+ type: "array",
509
+ items: { type: "string" },
510
+ maxItems: 20,
511
+ description: "The user ids it mentions"
512
+ }
513
+ },
514
+ additionalProperties: false
515
+ })
516
+ },
517
+ responses: {
518
+ 201: {
519
+ description: "The comment",
520
+ content: json(comment)
521
+ },
522
+ ...keyErrors,
523
+ 403: error("Forbidden")
524
+ }
525
+ }
526
+ },
527
+ [`/${entry.name}/{key}:mentionable`]: { get: {
528
+ operationId: `mentionable_${entry.name}`,
529
+ tags: tag,
530
+ summary: `The users a comment on a ${entry.name} record may mention`,
531
+ description: "At most 8, by name: users whose name contains q who may read the record, without the caller.",
532
+ parameters: [param("key"), query("q", "Part of the name")],
533
+ responses: {
534
+ 200: {
535
+ description: "The users",
536
+ content: json({
537
+ type: "object",
538
+ required: ["items"],
539
+ properties: { items: {
540
+ type: "array",
541
+ items: person
542
+ } }
543
+ })
544
+ },
545
+ ...keyErrors,
546
+ 403: error("Forbidden")
547
+ }
548
+ } }
549
+ },
399
550
  [`/${entry.name}:facets`]: { get: {
400
551
  operationId: `facets_${entry.name}`,
401
552
  tags: tag,
@@ -1,7 +1,7 @@
1
1
  import { AdminAuth } from "./better-auth/create-auth.js";
2
2
  import { EventsOptions } from "./changes/events-options.js";
3
- import { HistoryOptions } from "./history/options.js";
4
3
  import { Authenticator } from "./types.js";
4
+ import { HistoryOptions } from "./history/options.js";
5
5
  import { WebhooksOptions } from "./webhooks/table/options.js";
6
6
  import { FilesOptions } from "./files/options.js";
7
7
  import { FunctionSource } from "./functions/define-function.js";
@@ -1,9 +1,9 @@
1
1
  import { notFound } from "./problem.js";
2
+ import { tenantScope } from "./tenant.js";
3
+ import { recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields } from "./request-access.js";
2
4
  import { presentFiles } from "./files/present.js";
3
5
  import { pickFields } from "./rows.js";
4
6
  import { fetchRecord } from "./records.js";
5
- import { tenantScope } from "./tenant.js";
6
- import { recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields } from "./request-access.js";
7
7
  import { rowPermissions } from "./row-permissions.js";
8
8
  import { readTransaction } from "./transactions.js";
9
9
  //#region src/read-service.ts
@@ -1,6 +1,6 @@
1
1
  import { badRequest } from "./problem.js";
2
- import { exposeRow } from "./rows.js";
3
2
  import { readScope, requestAccess } from "./request-access.js";
3
+ import { exposeRow } from "./rows.js";
4
4
  import { checkFilter, createWhere } from "@protobase/schema";
5
5
  import { listQuery } from "@protobase/query";
6
6
  //#region src/related-labels.ts
@@ -9,14 +9,38 @@ const withoutSensitive = (model) => ({
9
9
  ...model,
10
10
  fields: Object.fromEntries(Object.entries(model.fields).filter(([, field]) => !field.sensitive))
11
11
  });
12
- const requestAccess = async (deps, entry, session) => {
13
- const resolved = await resolveAccess(entry.source, {
12
+ const withoutAnonymousCreate = (resolved, session) => session.anonymous ? {
13
+ ...resolved,
14
+ operations: {
15
+ ...resolved.operations,
16
+ create: false
17
+ },
18
+ writableFields: {
19
+ ...resolved.writableFields,
20
+ create: []
21
+ }
22
+ } : resolved;
23
+ const throughForm = (resolved, form) => {
24
+ const create = resolved.writableFields.create.filter((name) => form.fields.includes(name));
25
+ return {
26
+ ...resolved,
27
+ readableFields: [.../* @__PURE__ */ new Set([...resolved.readableFields, ...create])],
28
+ writableFields: {
29
+ create,
30
+ update: []
31
+ }
32
+ };
33
+ };
34
+ /** `form` is the public form a visitor sends, the only way a caller without a token creates. */
35
+ const requestAccess = async (deps, entry, session, form) => {
36
+ const all = await resolveAccess(entry.source, {
14
37
  user: {
15
38
  id: session.user.id,
16
39
  roles: session.user.roles
17
40
  },
18
41
  ...organizationOf(session)
19
42
  }, deps.accessOptions);
43
+ const resolved = form ? throughForm(all, form) : withoutAnonymousCreate(all, session);
20
44
  checkOwnerId(entry, session.user, resolved);
21
45
  const readable = restrictModel(entry.model, resolved);
22
46
  return {
@@ -9,6 +9,10 @@ import { returnPreference } from "./preferences.js";
9
9
  import { resolveTarget } from "./target.js";
10
10
  import { revealField } from "../reveal-service.js";
11
11
  import { readHistory } from "../history/read-history.js";
12
+ import { mentionable } from "../comments/mentionable.js";
13
+ import { postComment } from "../comments/post-comment.js";
14
+ import { readComments } from "../comments/read-comments.js";
15
+ import { generateField } from "../ai/generate-route.js";
12
16
  import { Hono } from "hono";
13
17
  //#region src/routes/record.ts
14
18
  const recordRoutes = (deps) => {
@@ -16,6 +20,8 @@ const recordRoutes = (deps) => {
16
20
  app.get("/:resource/:key", async (c) => {
17
21
  const { entry } = resolveTarget(deps, c.req.param("resource"));
18
22
  if (c.req.param("key").endsWith(":history")) return c.json(await readHistory(deps, entry, c.get("session"), keyFromPath(c, entry, ":history"), c.req.query()));
23
+ if (c.req.param("key").endsWith(":comments")) return c.json(await readComments(deps, entry, c.get("session"), keyFromPath(c, entry, ":comments"), c.req.query()));
24
+ if (c.req.param("key").endsWith(":mentionable")) return c.json(await mentionable(deps, entry, c.get("session"), keyFromPath(c, entry, ":mentionable"), c.req.query()));
19
25
  const deleted = deletedRows(entry.model, c.req.query("show_deleted") === "true");
20
26
  const found = await readRecord(deps, entry, c.get("session"), keyFromPath(c, entry), deleted);
21
27
  const columns = selectedFields({
@@ -40,6 +46,17 @@ const recordRoutes = (deps) => {
40
46
  app.post("/:resource/:key", async (c) => {
41
47
  const { entry } = resolveTarget(deps, c.req.param("resource"));
42
48
  if (c.req.param("key").endsWith(":reveal")) return reveal(c, entry);
49
+ if (c.req.param("key").endsWith(":comments")) {
50
+ const body = await c.req.json();
51
+ return c.json(await postComment(deps, entry, c.get("session"), keyFromPath(c, entry, ":comments"), body, c.req.url), 201);
52
+ }
53
+ if (c.req.param("key").endsWith(":generate")) {
54
+ const generated = await generateField(deps, entry, c.get("session"), keyFromPath(c, entry, ":generate"), await c.req.json(), c.req.raw.signal);
55
+ return c.json({
56
+ ...generated.record,
57
+ etag: generated.etag
58
+ }, 200, { ETag: generated.etag });
59
+ }
43
60
  if (!c.req.param("key").endsWith(":undelete")) throw notFound("There is no such method");
44
61
  const restored = await runWrite(deps, entry, c.get("session"), {
45
62
  operation: "undelete",
@@ -1,9 +1,9 @@
1
- import { aliasedColumn, columnId, scopeConditions, tableId, whereClause } from "./sql-parts.js";
2
- import { fetchRecord } from "./records.js";
3
1
  import { tenantScope } from "./tenant.js";
4
2
  import { recordDecision } from "./request-access.js";
5
- import { compileFilter } from "@protobase/query";
3
+ import { aliasedColumn, columnId, scopeConditions, tableId, whereClause } from "./sql-parts.js";
4
+ import { fetchRecord } from "./records.js";
6
5
  import { sql } from "kysely";
6
+ import { compileFilter } from "@protobase/query";
7
7
  //#region src/row-permissions.ts
8
8
  const keyOf = (access, row) => JSON.stringify(access.full.primaryKey.map((name) => String(row[name])));
9
9
  const matching = async (trx, access, rows, filter) => {
package/dist/sql-parts.js CHANGED
@@ -1,5 +1,5 @@
1
- import { compileFilter } from "@protobase/query";
2
1
  import { sql } from "kysely";
2
+ import { compileFilter } from "@protobase/query";
3
3
  //#region src/sql-parts.ts
4
4
  const etagAlias = "__pb_etag";
5
5
  const tableId = (model) => {
package/dist/types.d.ts CHANGED
@@ -35,6 +35,8 @@ export type WriteEvent = {
35
35
  resource: ResourceModel;
36
36
  user: User;
37
37
  tenant?: TenantValue;
38
+ /** The writer is a visitor without a token, through a public form. */
39
+ anonymous?: true;
38
40
  before?: Row;
39
41
  after?: Row;
40
42
  };
@@ -20,6 +20,11 @@ const isObject = (value) => typeof value === "object" && value !== null && !Arra
20
20
  const reserved = ["etag", "permissions"];
21
21
  const isEmptyText = (field, value) => field.type === "text" && !field.nullable && value === "";
22
22
  /**
23
+ * Whether a create must give the field, as `validateBody` decides when it is missing: the database supplies no value,
24
+ * it may not be null, and its schema has no default of its own (`.default()`).
25
+ */
26
+ const requiredOnCreate = (field, schema) => !(field.default && "db" in field.default) && !field.nullable && !schema.safeParse(void 0).success;
27
+ /**
23
28
  * Checks a create or update body for this caller. A field the caller cannot read is an unknown field, one they can read
24
29
  * but not write is read-only (the tenant column and the API's own `etag` and `permissions` keys are always read-only);
25
30
  * every provided value goes through its field's zod schema, then `.validate()` rules run on the result. An empty text
@@ -92,4 +97,4 @@ const validateBody = (access, mode, body, current = {}) => {
92
97
  return values;
93
98
  };
94
99
  //#endregion
95
- export { validateBody };
100
+ export { requiredOnCreate, validateBody };
@@ -1,19 +1,26 @@
1
- import { relatedErrors } from "./view-related.js";
1
+ import { openErrors, relatedErrors } from "./view-related.js";
2
2
  import { cardPhotosErrors } from "./view-card-photos.js";
3
3
  import { cardFavoriteErrors } from "./view-card-favorite.js";
4
4
  import { searchesOfFor } from "./view-saved-search.js";
5
5
  import { relatedColumnErrors } from "./view-related-column.js";
6
6
  import { resolveFieldPath } from "@protobase/layout";
7
- import { addressErrors, chartFields, hrefPlaceholders, navRecentErrors, pruneByShape, shapes, tableDisplay } from "@protobase/schema/internal";
7
+ import { addressErrors, chartFields, hrefPlaceholders, navRecentErrors, promptFields, pruneByShape, shapes, tableDisplay } from "@protobase/schema/internal";
8
8
  //#region src/view-access.ts
9
9
  const pruneRelated = (item, resource, models) => {
10
- const { columns, ...rest } = item;
10
+ const { columns, open, ...rest } = item;
11
11
  const visible = models[item.resource] ? columns?.filter((column) => resolveFieldPath(models, item.resource, column).ok) : void 0;
12
12
  const narrowed = visible && visible.length > 0 ? {
13
13
  ...rest,
14
14
  columns: visible
15
15
  } : rest;
16
- return relatedErrors(narrowed, resource, models).length === 0 ? narrowed : void 0;
16
+ const opened = open !== void 0 && openErrors({
17
+ ...narrowed,
18
+ open
19
+ }, models).length === 0 ? {
20
+ ...narrowed,
21
+ open
22
+ } : narrowed;
23
+ return relatedErrors(opened, resource, models).length === 0 ? opened : void 0;
17
24
  };
18
25
  const pruneGallery = (item, resource, model, models) => {
19
26
  const image = item.image !== void 0 && Object.hasOwn(model.fields, item.image) ? item.image : void 0;
@@ -101,6 +108,15 @@ const listShape = (resource, model, models, prompted) => shapes.object({
101
108
  badge: shapes.field,
102
109
  meta: shapes.list(shapes.field)
103
110
  }, { requires: ["by"] }),
111
+ calendar: shapes.object({
112
+ kind: shapes.data,
113
+ name: shapes.data,
114
+ label: shapes.data,
115
+ icon: shapes.data,
116
+ start: shapes.field,
117
+ end: shapes.field,
118
+ title: shapes.field
119
+ }, { requires: ["start"] }),
104
120
  custom: shapes.object({
105
121
  kind: shapes.data,
106
122
  name: shapes.data,
@@ -178,6 +194,11 @@ const layoutShape = (resource, model, models) => shapes.oneOf({
178
194
  title: shapes.data,
179
195
  limit: shapes.data,
180
196
  when: shapes.data
197
+ }),
198
+ comments: shapes.object({
199
+ kind: shapes.data,
200
+ title: shapes.data,
201
+ when: shapes.data
181
202
  })
182
203
  });
183
204
  const linkShape = (model, models) => shapes.map((run) => {
@@ -186,6 +207,12 @@ const linkShape = (model, models) => shapes.map((run) => {
186
207
  source: model
187
208
  }).length === 0 : hrefPlaceholders(run.href).every((name) => Object.hasOwn(model.fields, name))) ? run : void 0;
188
209
  });
210
+ const aiShape = (model) => shapes.map((run) => {
211
+ return [run.writes, ...promptFields(run.prompt ?? "")].every((name) => Object.hasOwn(model.fields, name)) ? {
212
+ kind: "ai",
213
+ writes: run.writes
214
+ } : void 0;
215
+ });
189
216
  const inputsShape = (models) => shapes.map((inputs) => inputs.every((input) => input.resource === void 0 || Object.hasOwn(models, input.resource)) ? inputs : void 0);
190
217
  const actionShape = (model, models) => shapes.object({
191
218
  name: shapes.data,
@@ -199,7 +226,8 @@ const actionShape = (model, models) => shapes.object({
199
226
  values: shapes.keys({ all: true })
200
227
  }, { requires: ["values"] }),
201
228
  delete: shapes.data,
202
- link: linkShape(model, models)
229
+ link: linkShape(model, models),
230
+ ai: aiShape(model)
203
231
  }),
204
232
  inputs: inputsShape(models)
205
233
  }, { requires: ["run", "inputs"] });
@@ -235,7 +263,8 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
235
263
  searchesOf: shapes.map((searches) => searchesOfFor(searches, resource, models)),
236
264
  import: shapes.data,
237
265
  export: shapes.data,
238
- create: shapes.data
266
+ create: shapes.data,
267
+ forms: shapes.map(() => void 0)
239
268
  });
240
269
  /**
241
270
  * The view with every reference to a field the caller cannot read removed: list columns, sort, search and displays,
@@ -34,6 +34,18 @@ const throughErrors = (through, resource, models) => {
34
34
  ...filterErrors(via, through.filter).map((error) => `filter: ${error}`)
35
35
  ].map((error) => `through: ${error}`);
36
36
  };
37
+ /** What is wrong with the relation a related section's rows open; empty without one. */
38
+ const openErrors = (item, models) => {
39
+ const target = models[item.resource];
40
+ if (item.open === void 0 || !target) return [];
41
+ const field = Object.hasOwn(target.fields, item.open) ? target.fields[item.open] : void 0;
42
+ if (!field) return [`open: unknown field "${item.open}" on ${target.name}`];
43
+ if (field.type !== "relation" || !field.relation) return [`open: "${item.open}" on ${target.name} is not a relation`];
44
+ if (item.open === item.field) return [`open: "${item.open}" is the field that matches this record`];
45
+ const far = models[field.relation.resource];
46
+ if (!far) return [`open: unknown resource "${field.relation.resource}"`];
47
+ return far.primaryKey.length === 1 ? [] : [`open: ${far.name} has a composite key, so its records cannot be opened from "${item.open}"`];
48
+ };
37
49
  /**
38
50
  * What is wrong with a related section on the record page of `resource`, against these models; empty when it can be
39
51
  * shown. Against the full models it checks the config; against a caller's models, whether that caller can use it.
@@ -52,8 +64,9 @@ const relatedErrors = (item, resource, models) => {
52
64
  if (!resolved.ok) return [`column: ${resolved.message}`];
53
65
  return resolved.steps.at(-1).field.sensitive ? [`column: "${column}" is sensitive, so it cannot be listed`] : [];
54
66
  }),
55
- ...item.limit === void 0 || Number.isInteger(item.limit) && item.limit >= 1 && item.limit <= 100 ? [] : ["limit must be a whole number from 1 to 100"]
67
+ ...item.limit === void 0 || Number.isInteger(item.limit) && item.limit >= 1 && item.limit <= 100 ? [] : ["limit must be a whole number from 1 to 100"],
68
+ ...openErrors(item, models)
56
69
  ];
57
70
  };
58
71
  //#endregion
59
- export { matchErrors, relatedErrors, sortErrors, throughKey };
72
+ export { matchErrors, openErrors, relatedErrors, sortErrors, throughKey };
@@ -1,7 +1,7 @@
1
1
  import { EventCodeError } from "../changes/event-code-error.js";
2
2
  import { eventPatternMatches } from "./subscription.js";
3
- import { evaluateFilter } from "@protobase/schema";
4
3
  import { checkEventFilter } from "@protobase/schema/internal";
4
+ import { evaluateFilter } from "@protobase/schema";
5
5
  //#region src/webhooks/route-events.ts
6
6
  const declaredEvents = (models) => models.flatMap((model) => (model.events ?? []).map((event) => ({
7
7
  model,
@@ -1,8 +1,8 @@
1
1
  import { quoteIdent } from "../../changes/capture-sql.js";
2
2
  import { changesSchema } from "../../changes/events-options.js";
3
+ import { migrationCommand } from "../../changes/capture-schema.js";
3
4
  import { webhooksSchema } from "./options.js";
4
5
  import { commandsSql, deliveriesSql, deliveryLogSql, subscriptionsSql, webhooksComment } from "./schema-sql.js";
5
- import { migrationCommand } from "../../changes/capture-schema.js";
6
6
  import { sql } from "kysely";
7
7
  //#region src/webhooks/table/schema.ts
8
8
  const functionNames = ["subscription_commands", "deliveries_resend"];
@@ -1,6 +1,6 @@
1
+ import { betterAuthOwnerRoles } from "./owner-roles.js";
1
2
  import { webhooksSchema } from "./options.js";
2
3
  import { checkWebhookUrl } from "./address-check.js";
3
- import { betterAuthOwnerRoles } from "./owner-roles.js";
4
4
  import { tableSubscriptions } from "./table-subscriptions.js";
5
5
  //#region src/webhooks/table/webhook-table.ts
6
6
  /**