@protobase/server 0.51.3 → 0.52.0

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 (96) hide show
  1. package/dist/aggregate-groups.js +2 -2
  2. package/dist/aggregate-service.js +4 -4
  3. package/dist/aggregate-splits.js +1 -1
  4. package/dist/assistant/batch-approval.js +49 -51
  5. package/dist/assistant/batch-before.js +38 -0
  6. package/dist/assistant/batch-echo.js +36 -0
  7. package/dist/assistant/batch-write-args.js +14 -5
  8. package/dist/assistant/batch-write-tool.js +30 -11
  9. package/dist/assistant/budgeted-page.js +30 -0
  10. package/dist/assistant/comment-tools.d.ts +20 -0
  11. package/dist/assistant/comment-tools.js +120 -0
  12. package/dist/assistant/count-records-tool.d.ts +5 -0
  13. package/dist/assistant/count-records-tool.js +84 -0
  14. package/dist/assistant/query-result-budget.js +32 -0
  15. package/dist/assistant/query-tools.js +7 -7
  16. package/dist/assistant/record-before.js +2 -1
  17. package/dist/assistant/record-field-piece.js +54 -0
  18. package/dist/assistant/record-tool-args.js +6 -3
  19. package/dist/assistant/record-tools.d.ts +4 -1
  20. package/dist/assistant/record-tools.js +17 -6
  21. package/dist/assistant/records.d.ts +19 -0
  22. package/dist/assistant/records.js +20 -13
  23. package/dist/assistant/result-budget.js +72 -0
  24. package/dist/assistant/tools.d.ts +3 -0
  25. package/dist/assistant/write-echo.js +41 -0
  26. package/dist/assistant/write-messages.js +26 -6
  27. package/dist/assistant/write-prompts.js +37 -12
  28. package/dist/assistant/write-record-tools.js +15 -6
  29. package/dist/batch/run-batch.js +1 -1
  30. package/dist/better-auth/auth-settings.d.ts +1 -1
  31. package/dist/changes/capture-schema.js +1 -1
  32. package/dist/changes/resource-change.d.ts +4 -1
  33. package/dist/changes/resource-change.js +27 -3
  34. package/dist/comments/post-comment.js +1 -1
  35. package/dist/comments/read-comments.js +1 -1
  36. package/dist/comments/tool-comment-types.d.ts +28 -0
  37. package/dist/comments/tool-comments.js +30 -0
  38. package/dist/create-admin.d.ts +5 -2
  39. package/dist/create-admin.js +20 -10
  40. package/dist/deps.d.ts +1 -1
  41. package/dist/encryption/scan.js +1 -1
  42. package/dist/error-handler.js +3 -2
  43. package/dist/etag.js +1 -1
  44. package/dist/files/download.js +1 -1
  45. package/dist/files/file-cleanup.d.ts +1 -1
  46. package/dist/functions/records.d.ts +1 -1
  47. package/dist/functions/records.js +2 -2
  48. package/dist/history/read-history.js +2 -2
  49. package/dist/index.d.ts +9 -3
  50. package/dist/index.js +9 -4
  51. package/dist/jobs/run-job.d.ts +1 -1
  52. package/dist/list-photos.js +2 -2
  53. package/dist/list-service.js +6 -6
  54. package/dist/map-service.js +2 -2
  55. package/dist/mcp/mcp-context.d.ts +7 -4
  56. package/dist/mcp/mcp-context.js +6 -4
  57. package/dist/mcp/mcp-server.d.ts +3 -2
  58. package/dist/mcp/mcp-tools.d.ts +6 -1
  59. package/dist/mcp/mcp-tools.js +8 -4
  60. package/dist/package.js +1 -1
  61. package/dist/read-service.js +2 -2
  62. package/dist/realtime/broadcast-api.js +3 -3
  63. package/dist/realtime/broadcast-hub.js +15 -5
  64. package/dist/realtime/change-checker.js +1 -1
  65. package/dist/realtime/channel-access.js +1 -1
  66. package/dist/realtime/database-broadcasts.js +1 -1
  67. package/dist/realtime/presence-hub.js +74 -39
  68. package/dist/realtime/realtime-app.js +28 -4
  69. package/dist/realtime/realtime-routes.js +3 -5
  70. package/dist/realtime/realtime-socket.js +3 -1
  71. package/dist/realtime/relay/frame-cursor.js +38 -0
  72. package/dist/realtime/relay/presence-replica.js +57 -0
  73. package/dist/realtime/relay/realtime-options.d.ts +13 -0
  74. package/dist/realtime/relay/realtime-options.js +9 -0
  75. package/dist/realtime/relay/realtime-relay.js +104 -0
  76. package/dist/realtime/relay/relay-frame-sql.js +82 -0
  77. package/dist/realtime/relay/relay-presence-sql.js +71 -0
  78. package/dist/realtime/relay/relay-schema-sql.js +55 -0
  79. package/dist/realtime/relay/relay-schema.d.ts +7 -0
  80. package/dist/realtime/relay/relay-schema.js +18 -0
  81. package/dist/realtime/relay/relayed-hubs.js +61 -0
  82. package/dist/records.js +1 -1
  83. package/dist/related-labels.js +1 -1
  84. package/dist/rest/rest-context.js +1 -1
  85. package/dist/routes/batch.js +1 -1
  86. package/dist/routes/collection.js +2 -2
  87. package/dist/routes/record.js +3 -3
  88. package/dist/row-permissions.js +1 -1
  89. package/dist/sql-parts.js +1 -1
  90. package/dist/web-app/mcp-icons.d.ts +9 -0
  91. package/dist/web-app/mcp-icons.js +26 -0
  92. package/dist/webhooks/subscriber-view.js +27 -2
  93. package/dist/webhooks/webhooks.js +41 -12
  94. package/dist/write-pipeline.js +1 -1
  95. package/dist/write-shared.js +1 -1
  96. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  import { requestAccess } from "../request-access.js";
2
2
  import { EventCodeError } from "../changes/event-code-error.js";
3
+ import { currentRecord } from "../changes/resource-change.js";
3
4
  import { checkRecord, evaluateFilter } from "@protobase/schema";
4
5
  //#region src/webhooks/subscriber-view.ts
5
6
  /**
@@ -40,6 +41,28 @@ const subscriptionEvent = (change, event, { fields, thin = false } = {}) => ({
40
41
  transaction: change.transaction
41
42
  }
42
43
  });
44
+ /**
45
+ * A delivery's stored event as a change again, with its records as the model has them now, to shape it anew for the
46
+ * subscription as it is now: an event stored before a field became sensitive holds it. The database role is unknown.
47
+ */
48
+ const eventChange = (model, event) => {
49
+ const { data } = event;
50
+ return {
51
+ resource: data.resource,
52
+ key: data.key,
53
+ operation: data.operation,
54
+ ...data.before && { before: currentRecord(model, data.before) },
55
+ ...data.after && { after: currentRecord(model, data.after) },
56
+ changed: data.changed,
57
+ actor: data.actor,
58
+ source: data.source,
59
+ databaseRole: null,
60
+ ...data.tenant !== void 0 && { tenant: data.tenant },
61
+ transaction: data.transaction,
62
+ sequence: data.sequence,
63
+ at: event.timestamp
64
+ };
65
+ };
43
66
  const withUnknownFields = (model, record) => {
44
67
  const fields = Object.values(model.fields).filter((field) => field.sensitive || !Object.hasOwn(record, field.name));
45
68
  const unknown = { ...record };
@@ -96,7 +119,9 @@ const subscriberEvents = (deps) => {
96
119
  if (!accesses.has(cacheKey)) accesses.set(cacheKey, requestAccess(deps, entry, session));
97
120
  const readable = async () => {
98
121
  const access = await accesses.get(cacheKey);
99
- return await mayRead(access, change.after ?? change.before) ? access : void 0;
122
+ const record = change.after ?? change.before;
123
+ if (!record) return access.resolved.operations.read ? access : void 0;
124
+ return await mayRead(access, record) ? access : void 0;
100
125
  };
101
126
  const access = await readable().catch((error) => {
102
127
  throw new EventCodeError(`The access check of subscription "${subscription.name}" on ${change.resource} ${change.key}`, error);
@@ -109,4 +134,4 @@ const subscriberEvents = (deps) => {
109
134
  };
110
135
  };
111
136
  //#endregion
112
- export { subscriberEvents, subscriberSession, subscriptionEvent };
137
+ export { eventChange, subscriberEvents, subscriberSession, subscriptionEvent };
@@ -6,11 +6,11 @@ import { coalesce } from "../changes/coalesce.js";
6
6
  import { EventCodeError } from "../changes/event-code-error.js";
7
7
  import { eventMatcher } from "../changes/match-events.js";
8
8
  import { deleteOutbox, readOutbox } from "../changes/outbox.js";
9
- import { resourceChange } from "../changes/resource-change.js";
9
+ import { currentChange, resourceChange } from "../changes/resource-change.js";
10
10
  import { callerRecords } from "../functions/records.js";
11
11
  import { writeHistory } from "../history/write-history.js";
12
12
  import { maxTries, nextRetryDelay } from "./retry-schedule.js";
13
- import { subscriberEvents } from "./subscriber-view.js";
13
+ import { eventChange, subscriberEvents } from "./subscriber-view.js";
14
14
  import { attemptDelivery } from "./attempt.js";
15
15
  import { subscriptionProblems, subscriptionRouter } from "./route-events.js";
16
16
  import { tableSubscriptionName } from "./table/options.js";
@@ -51,7 +51,8 @@ const deliveryGroup = ({ subscription, change }) => `${subscription.name}:${chan
51
51
  * only: the others still receive it, and the retry delivers to that subscription alone. Any other error fails the
52
52
  * whole dispatch, which is retried.
53
53
  * - `webhooks.deliver`, one try of an unordered delivery; a failure queues the next try on the Standard Webhooks
54
- * schedule, or Retry-After, and the last one is kept as failed.
54
+ * schedule, or Retry-After, and the last one is kept as failed. Each try, and each of the job below, shapes its event
55
+ * again for the subscription as it is now, and sends nothing when it may no longer read the record.
55
56
  * - `webhooks.deliver-ordered`, the ordered deliveries of one dispatch for one subscription and record, retried with
56
57
  * doubling waits up to a day, holding back the record's later deliveries to that subscription meanwhile.
57
58
  *
@@ -103,8 +104,25 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
103
104
  const found = id === void 0 ? byName.get(name) : await table?.subscriptions.find(db, id);
104
105
  if (!found) throw new FatalJobError(`The subscription "${name}" no longer exists`);
105
106
  if (found.options.enabled === false) throw new FatalJobError(`The subscription "${name}" is disabled`);
107
+ const problems = id === void 0 ? [] : subscriptionProblems([found], models, []);
108
+ if (problems.length > 0) throw new FatalJobError(problems.join("; "));
106
109
  return found;
107
110
  };
111
+ const modelOf = (resource) => {
112
+ const model = deps.registry.find(resource)?.model;
113
+ if (!model) throw new FatalJobError(`The resource "${resource}" no longer exists`);
114
+ return model;
115
+ };
116
+ const reshaper = () => {
117
+ const eventOf = subscriberEvents(deps);
118
+ return async (subscription, stored) => {
119
+ const shaped = await eventOf(subscription, eventChange(modelOf(stored.data.resource), stored), stored.type);
120
+ return shaped && {
121
+ ...shaped,
122
+ id: stored.id
123
+ };
124
+ };
125
+ };
108
126
  const deliver = defineJob({
109
127
  name: "webhooks.deliver",
110
128
  maxAttempts: 3,
@@ -113,13 +131,16 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
113
131
  concurrency: parallelAttempts,
114
132
  run: async (payload, { db, signal }) => {
115
133
  const subscription = await subscriptionOf(payload.subscription, db);
116
- const result = await attempt(subscription, payload.event, db, payload.attempt, signal);
117
- report(payload.subscription, payload.event, payload.attempt, result);
134
+ const event = await reshaper()(subscription, payload.event);
135
+ if (!event) return;
136
+ const result = await attempt(subscription, event, db, payload.attempt, signal);
137
+ report(payload.subscription, event, payload.attempt, result);
118
138
  if (result.ok) return;
119
139
  const delay = result.final ? void 0 : nextRetryDelay(payload.attempt, result.retryAfterMs);
120
140
  if (delay === void 0) throw new FatalJobError(result.error);
121
141
  await deliver.enqueue(db, {
122
142
  ...payload,
143
+ event,
123
144
  attempt: payload.attempt + 1,
124
145
  lastError: result.error
125
146
  }, {
@@ -138,7 +159,13 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
138
159
  concurrency: parallelAttempts,
139
160
  run: async (payload, { db, signal, job }) => {
140
161
  const subscription = await subscriptionOf(payload.subscription, db);
141
- for (const event of payload.events) {
162
+ const reshape = reshaper();
163
+ const events = [];
164
+ for (const stored of payload.events) {
165
+ const event = await reshape(subscription, stored);
166
+ if (event) events.push(event);
167
+ }
168
+ for (const event of events) {
142
169
  const result = await attempt(subscription, event, db, job.attempt, signal);
143
170
  report(payload.subscription, event, job.attempt, result);
144
171
  if (result.ok) continue;
@@ -147,11 +174,12 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
147
174
  }
148
175
  }
149
176
  });
150
- const changeDeliveries = async (db) => {
151
- const route = subscriptionRouter(await current(db), models);
177
+ const changeDeliveries = async (db, only) => {
178
+ const all = await current(db);
179
+ const route = subscriptionRouter(only === void 0 ? all : all.filter(({ name }) => name === only), models);
152
180
  const eventOf = subscriberEvents(deps);
153
- return async (change, only) => {
154
- const routes = route(change, matchers.get(change.resource)(change)).filter(({ subscription }) => only === void 0 || subscription.name === only);
181
+ return async (change) => {
182
+ const routes = route(change, matchers.get(change.resource)(change));
155
183
  const deliveries = [];
156
184
  const failures = [];
157
185
  const failed = /* @__PURE__ */ new Set();
@@ -195,8 +223,9 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
195
223
  name: "webhooks.dispatch-change",
196
224
  maxAttempts: 1,
197
225
  timeout: "5 minutes",
198
- run: async ({ change, subscription: only }, { db }) => {
199
- const { deliveries, failures } = await (await changeDeliveries(db))(change, only);
226
+ run: async ({ change: kept, subscription: only }, { db }) => {
227
+ const change = currentChange(modelOf(kept.resource), kept);
228
+ const { deliveries, failures } = await (await changeDeliveries(db, only))(change);
200
229
  if (only !== void 0 && failures.length > 0) throw failures[0].error;
201
230
  await db.transaction().execute(async (trx) => {
202
231
  for (const { subscription, event } of deliveries.filter(({ subscription }) => !subscription.options.ordered)) {
@@ -1,10 +1,10 @@
1
1
  import { HttpProblem, badRequest } from "./problem.js";
2
2
  import { tenantScope } from "./tenant.js";
3
3
  import { requestAccess, requireOperation, targetFilter, writableFields } from "./request-access.js";
4
+ import { writeTransaction } from "./transactions.js";
4
5
  import { actorSettings, wakeDispatch } from "./changes/actor.js";
5
6
  import { isDatabaseError } from "./error-handler.js";
6
7
  import { attachFiles, scheduleRemovedFiles } from "./files/attach.js";
7
- import { writeTransaction } from "./transactions.js";
8
8
  import { bindingOf, pendingBinding } from "./encryption/binding.js";
9
9
  import { deleteRecord, fetchRecord, insertRecord, restoreRecord, updateRecord } from "./records.js";
10
10
  import { assertWritten, checkParents, checkPrecondition, decide, denied, firstKeyField, keyOf, missing, notifier, shown } from "./write-shared.js";
@@ -2,9 +2,9 @@ import { maskEncrypted } from "./encryption/mask-stored.js";
2
2
  import { forbidden, notFound, preconditionFailed, preconditionRequired } from "./problem.js";
3
3
  import { tenantScope } from "./tenant.js";
4
4
  import { andFilters, recordDecision } from "./request-access.js";
5
+ import { pickFields } from "./rows.js";
5
6
  import { removedFiles } from "./files/attach.js";
6
7
  import { ifMatchSatisfied } from "./etag.js";
7
- import { pickFields } from "./rows.js";
8
8
  import { fetchRecord } from "./records.js";
9
9
  import { parentGuard } from "./parent-access.js";
10
10
  import { presentFiles } from "./files/present.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.51.3",
3
+ "version": "0.52.0",
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": {
@@ -40,9 +40,9 @@
40
40
  "@better-auth/passkey": "^1.7.12",
41
41
  "@better-auth/sso": "^1.7.12",
42
42
  "@modelcontextprotocol/server": "^2.3.1",
43
- "@protobase/layout": "0.51.3",
44
- "@protobase/query": "0.51.3",
45
- "@protobase/schema": "0.51.3",
43
+ "@protobase/layout": "0.52.0",
44
+ "@protobase/query": "0.52.0",
45
+ "@protobase/schema": "0.52.0",
46
46
  "@react-email/render": "^2.1.0",
47
47
  "@scalar/hono-api-reference": "^0.12.10",
48
48
  "@sentry/core": "^11.7.0",