@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
|
|
44
|
-
const fields = Object.values(model.fields).filter((field) => field.
|
|
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
|
-
|
|
54
|
+
read.push(field.name);
|
|
55
|
+
throw error(field.name);
|
|
51
56
|
}
|
|
52
57
|
});
|
|
53
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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.
|
|
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.
|
|
39
|
-
"@protobase/query": "0.
|
|
40
|
-
"@protobase/schema": "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",
|