@protobase/server 0.35.0 → 0.36.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.
@@ -40,20 +40,30 @@ const subscriptionEvent = (change, event, { fields, thin = false } = {}) => ({
40
40
  transaction: change.transaction
41
41
  }
42
42
  });
43
- const withUnknownEncrypted = (model, record) => {
44
- const fields = Object.values(model.fields).filter((field) => field.encrypted);
45
- if (fields.length === 0) return record;
43
+ const withUnknownFields = (model, record) => {
44
+ const fields = Object.values(model.fields).filter((field) => field.sensitive || !Object.hasOwn(record, field.name));
46
45
  const unknown = { ...record };
46
+ const read = [];
47
+ const error = (name) => {
48
+ const field = model.fields[name];
49
+ return /* @__PURE__ */ new Error(`a subscription's access check cannot tell whether the ${field.sensitive ? "sensitive field" : "field"} "${name}" is set${field.sensitive ? "" : ", which the captured change does not hold"}`);
50
+ };
47
51
  for (const field of fields) Object.defineProperty(unknown, field.name, {
48
52
  enumerable: true,
49
53
  get: () => {
50
- throw new Error(`a subscription's access check cannot tell whether the encrypted field "${field.name}" is set`);
54
+ read.push(field.name);
55
+ throw error(field.name);
51
56
  }
52
57
  });
53
- return unknown;
58
+ const check = () => {
59
+ if (read.length > 0) throw error(read[0]);
60
+ };
61
+ return {
62
+ record: unknown,
63
+ check
64
+ };
54
65
  };
55
- const mayRead = async (access, captured) => {
56
- const record = withUnknownEncrypted(access.full, captured);
66
+ const decide = async (access, record) => {
57
67
  const { resolved, session, entry } = access;
58
68
  if (!resolved.operations.read) return false;
59
69
  if (resolved.rowFilter.read && evaluateFilter(resolved.rowFilter.read, record) !== true) return false;
@@ -64,6 +74,12 @@ const mayRead = async (access, captured) => {
64
74
  };
65
75
  return checkRecord(entry.source, context, "read", record, void 0, access.deps.accessOptions);
66
76
  };
77
+ const mayRead = async (access, captured) => {
78
+ const { record, check } = withUnknownFields(access.full, captured);
79
+ const allowed = await decide(access, record);
80
+ check();
81
+ return allowed;
82
+ };
67
83
  /**
68
84
  * Builds each subscriber's event, under the access rules of the session it delivers as, resolved once per subscription
69
85
  * and resource: `undefined` when it may not read the record, after the change or before a delete.
@@ -47,7 +47,9 @@ const deliveryGroup = ({ subscription, change }) => `${subscription.name}:${chan
47
47
  * before that commits leaves the rows, and the job's retry delivers them again, under the same `webhook-id`.
48
48
  * - `webhooks.dispatch-change`, one change whose app code threw in dispatch: an event's `match` or filter, a
49
49
  * subscription's `when`, or an access rule of its roles. Kept as failed, so dispatch moves on to later changes; a
50
- * retry after the fix queues the change's deliveries. Any other error fails the whole dispatch, which is retried.
50
+ * retry after the fix queues the change's deliveries. An access rule's failure keeps the change for that subscription
51
+ * only: the others still receive it, and the retry delivers to that subscription alone. Any other error fails the
52
+ * whole dispatch, which is retried.
51
53
  * - `webhooks.deliver`, one try of an unordered delivery; a failure queues the next try on the Standard Webhooks
52
54
  * schedule, or Retry-After, and the last one is kept as failed.
53
55
  * - `webhooks.deliver-ordered`, the ordered deliveries of one dispatch for one subscription and record, retried with
@@ -148,17 +150,32 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
148
150
  const changeDeliveries = async (db) => {
149
151
  const route = subscriptionRouter(await current(db), models);
150
152
  const eventOf = subscriberEvents(deps);
151
- return async (change) => {
153
+ return async (change, only) => {
154
+ const routes = route(change, matchers.get(change.resource)(change)).filter(({ subscription }) => only === void 0 || subscription.name === only);
152
155
  const deliveries = [];
153
- for (const { subscription, event } of route(change, matchers.get(change.resource)(change))) {
154
- const shaped = await eventOf(subscription, change, event);
156
+ const failures = [];
157
+ const failed = /* @__PURE__ */ new Set();
158
+ for (const { subscription, event } of routes) {
159
+ if (failed.has(subscription.name)) continue;
160
+ const shaped = await eventOf(subscription, change, event).catch((error) => {
161
+ if (!(error instanceof EventCodeError)) throw error;
162
+ failed.add(subscription.name);
163
+ failures.push({
164
+ change,
165
+ subscription: subscription.name,
166
+ error
167
+ });
168
+ });
155
169
  if (shaped) deliveries.push({
156
170
  subscription,
157
171
  event: shaped,
158
172
  change
159
173
  });
160
174
  }
161
- return deliveries;
175
+ return {
176
+ deliveries: deliveries.filter(({ subscription }) => !failed.has(subscription.name)),
177
+ failures
178
+ };
162
179
  };
163
180
  };
164
181
  const queueOrdered = async (db, deliveries) => {
@@ -178,8 +195,9 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
178
195
  name: "webhooks.dispatch-change",
179
196
  maxAttempts: 1,
180
197
  timeout: "5 minutes",
181
- run: async ({ change }, { db }) => {
182
- const deliveries = await (await changeDeliveries(db))(change);
198
+ run: async ({ change, subscription: only }, { db }) => {
199
+ const { deliveries, failures } = await (await changeDeliveries(db))(change, only);
200
+ if (only !== void 0 && failures.length > 0) throw failures[0].error;
183
201
  await db.transaction().execute(async (trx) => {
184
202
  for (const { subscription, event } of deliveries.filter(({ subscription }) => !subscription.options.ordered)) {
185
203
  const payload = {
@@ -190,6 +208,10 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
190
208
  await deliver.enqueue(trx, payload, { key: deliveryKey(payload) });
191
209
  }
192
210
  await queueOrdered(trx, deliveries);
211
+ for (const { subscription } of failures) await dispatchChange.enqueue(trx, {
212
+ change,
213
+ subscription
214
+ });
193
215
  });
194
216
  }
195
217
  });
@@ -204,7 +226,9 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
204
226
  const deliveries = [];
205
227
  const failures = [];
206
228
  for (const change of changes) try {
207
- deliveries.push(...await deliveriesOf(change));
229
+ const routed = await deliveriesOf(change);
230
+ deliveries.push(...routed.deliveries);
231
+ failures.push(...routed.failures);
208
232
  } catch (error) {
209
233
  if (!(error instanceof EventCodeError)) throw error;
210
234
  failures.push({
@@ -232,7 +256,10 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
232
256
  });
233
257
  }
234
258
  await queueOrdered(trx, deliveries);
235
- for (const { change } of failures) await dispatchChange.enqueue(trx, { change });
259
+ for (const { change, subscription } of failures) await dispatchChange.enqueue(trx, {
260
+ change,
261
+ ...subscription !== void 0 && { subscription }
262
+ });
236
263
  await writeHistory(trx, schema, changes.filter((change) => deps.registry.find(change.resource)?.model.history));
237
264
  await writeNotices(trx, schema, changes.filter((change) => deps.registry.find(change.resource)?.model.live));
238
265
  await deleteOutbox(trx, schema, rows.map((row) => row.id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.35.0",
3
+ "version": "0.36.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": {
@@ -35,9 +35,9 @@
35
35
  "@better-auth/oauth-provider": "^1.7.12",
36
36
  "@better-auth/passkey": "^1.7.12",
37
37
  "@better-auth/sso": "^1.7.12",
38
- "@protobase/layout": "0.35.0",
39
- "@protobase/query": "0.35.0",
40
- "@protobase/schema": "0.35.0",
38
+ "@protobase/layout": "0.36.0",
39
+ "@protobase/query": "0.36.0",
40
+ "@protobase/schema": "0.36.0",
41
41
  "@react-email/render": "^2.1.0",
42
42
  "@scalar/hono-api-reference": "^0.12.10",
43
43
  "@sentry/core": "^11.7.0",