@protobase/server 0.8.2 → 0.9.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 (46) hide show
  1. package/dist/batch/run-batch.js +4 -2
  2. package/dist/changes/actor.js +7 -1
  3. package/dist/changes/capture-schema.js +1 -1
  4. package/dist/changes/capture-sql.js +13 -2
  5. package/dist/changes/coalesce.d.ts +4 -0
  6. package/dist/changes/coalesce.js +2 -1
  7. package/dist/changes/outbox.d.ts +14 -0
  8. package/dist/changes/outbox.js +51 -0
  9. package/dist/changes/resource-change.d.ts +2 -0
  10. package/dist/changes/resource-change.js +2 -1
  11. package/dist/create-admin.d.ts +10 -5
  12. package/dist/create-admin.js +20 -7
  13. package/dist/index.d.ts +10 -4
  14. package/dist/index.js +13 -8
  15. package/dist/jobs/current.d.ts +2 -0
  16. package/dist/jobs/current.js +3 -1
  17. package/dist/jobs/runtime.js +7 -5
  18. package/dist/project-config.d.ts +4 -1
  19. package/dist/project-config.js +10 -2
  20. package/dist/resource-source.d.ts +8 -3
  21. package/dist/resource-source.js +6 -3
  22. package/dist/types.d.ts +5 -1
  23. package/dist/webhooks/attempt.js +91 -0
  24. package/dist/webhooks/retry-schedule.js +40 -0
  25. package/dist/webhooks/route-events.js +61 -0
  26. package/dist/webhooks/standard-webhooks.d.ts +4 -0
  27. package/dist/webhooks/standard-webhooks.js +36 -0
  28. package/dist/webhooks/subscriber-view.js +76 -0
  29. package/dist/webhooks/subscription.d.ts +92 -0
  30. package/dist/webhooks/subscription.js +28 -0
  31. package/dist/webhooks/table/address-check.js +28 -0
  32. package/dist/webhooks/table/options.d.ts +23 -0
  33. package/dist/webhooks/table/options.js +14 -0
  34. package/dist/webhooks/table/owner-roles.js +23 -0
  35. package/dist/webhooks/table/plugin.d.ts +10 -0
  36. package/dist/webhooks/table/plugin.js +18 -0
  37. package/dist/webhooks/table/schema-sql.js +116 -0
  38. package/dist/webhooks/table/schema.d.ts +13 -0
  39. package/dist/webhooks/table/schema.js +44 -0
  40. package/dist/webhooks/table/table-subscriptions.js +77 -0
  41. package/dist/webhooks/table/validate-hook.js +32 -0
  42. package/dist/webhooks/table/webhook-table.js +23 -0
  43. package/dist/webhooks/test-event.js +48 -0
  44. package/dist/webhooks/webhooks.js +233 -0
  45. package/dist/write-pipeline.js +4 -2
  46. package/package.json +4 -4
@@ -2,7 +2,7 @@ import { HttpProblem, badRequest, notFound } from "../problem.js";
2
2
  import { problemOf } from "../error-handler.js";
3
3
  import { requestAccess } from "../request-access.js";
4
4
  import { writeTransaction } from "../transactions.js";
5
- import { actorSettings } from "../changes/actor.js";
5
+ import { actorSettings, wakeDispatch } from "../changes/actor.js";
6
6
  import { keyOf } from "../write-shared.js";
7
7
  import { writeIn } from "../write-pipeline.js";
8
8
  import { resolveData, resolveKey } from "./refs.js";
@@ -38,7 +38,7 @@ const runBatch = async (deps, session, operations) => {
38
38
  accesses.set(entry.name, access);
39
39
  return access;
40
40
  };
41
- return writeTransaction(deps.db, deps.statementTimeoutMs, async (trx) => {
41
+ const written = await writeTransaction(deps.db, deps.statementTimeoutMs, async (trx) => {
42
42
  const created = /* @__PURE__ */ new Map();
43
43
  const results = [];
44
44
  for (const [index, operation] of operations.entries()) try {
@@ -101,6 +101,8 @@ const runBatch = async (deps, session, operations) => {
101
101
  }
102
102
  return results;
103
103
  }, actorSettings(deps, session));
104
+ wakeDispatch(deps);
105
+ return written;
104
106
  };
105
107
  //#endregion
106
108
  export { runBatch };
@@ -1,3 +1,5 @@
1
+ import { runningJobs } from "../jobs/current.js";
2
+ import { dispatchJobName } from "./capture-sql.js";
1
3
  //#region src/changes/actor.ts
2
4
  /** The setting a write transaction sets so the change capture records who wrote, when a resource has events. */
3
5
  const actorSettings = (deps, session) => {
@@ -8,5 +10,9 @@ const actorSettings = (deps, session) => {
8
10
  };
9
11
  return { [deps.actorSetting]: JSON.stringify(actor) };
10
12
  };
13
+ /** After a write committed: has this process's dispatcher deliver its events now, rather than at its next poll. */
14
+ const wakeDispatch = (deps) => {
15
+ if (deps.actorSetting) runningJobs()?.wake(dispatchJobName);
16
+ };
11
17
  //#endregion
12
- export { actorSettings };
18
+ export { actorSettings, wakeDispatch };
@@ -1,5 +1,5 @@
1
- import { changesSchema } from "./events-options.js";
2
1
  import { quoteIdent, tableSql } from "./capture-sql.js";
2
+ import { changesSchema } from "./events-options.js";
3
3
  import { capturePlan, planSql, uncaptured } from "./capture-plan.js";
4
4
  import { sql } from "kysely";
5
5
  //#region src/changes/capture-schema.ts
@@ -1,3 +1,4 @@
1
+ import { jobsSchema } from "../jobs/schema.js";
1
2
  //#region src/changes/capture-sql.ts
2
3
  const quoteIdent = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
3
4
  const quoteLiteral = (text) => `'${text.replaceAll("'", "''")}'`;
@@ -31,10 +32,16 @@ const outboxSql = (schema) => `create table if not exists ${quoteIdent(schema)}.
31
32
  "dispatch_after" timestamptz not null default now(),
32
33
  "created_at" timestamptz not null default now()
33
34
  )`;
35
+ /** The job that turns captured changes into events and deliveries: one per writing transaction, run one at a time. */
36
+ const dispatchJobName = "changes.dispatch";
37
+ /** Every dispatch job's group in its ordered queue, so only one runs at a time, across processes too. */
38
+ const dispatchGroup = "changes";
34
39
  /**
35
40
  * The body of `capture()`: its arguments are the resource, the columns to keep and the sensitive columns, whose names
36
41
  * it lists when they change but whose values it never keeps. It skips a transaction that set `<schema>.capture` to
37
- * `off`, and an update that changed none of those columns.
42
+ * `off`, and an update that changed none of those columns. The first row a transaction captures enqueues a dispatch
43
+ * job in the same transaction; `<schema>.dispatch` remembers that for the rest of it, and a rolled-back savepoint
44
+ * forgets it with the job.
38
45
  */
39
46
  const captureBody = (schema) => `
40
47
  declare
@@ -71,6 +78,10 @@ begin
71
78
  changed,
72
79
  nullif(current_setting('${schema}.actor', true), '')::jsonb
73
80
  );
81
+ if current_setting('${schema}.dispatch', true) is distinct from 'queued' then
82
+ perform ${jobsSchema}.enqueue('${dispatchJobName}', null, null, null, '${dispatchGroup}');
83
+ perform set_config('${schema}.dispatch', 'queued', true);
84
+ end if;
74
85
  return null;
75
86
  end
76
87
  `;
@@ -105,4 +116,4 @@ const createTriggerSql = (schema, trigger, table) => {
105
116
  return `create trigger ${quoteIdent(trigger.name)} ${timing} on ${table} for each row${when} execute function ${quoteIdent(schema)}."capture"(${args})`;
106
117
  };
107
118
  //#endregion
108
- export { captureBody, captureFunctionSql, createTriggerSql, dropTriggerSql, outboxSql, quoteIdent, resourceTriggers, tableSql };
119
+ export { captureBody, captureFunctionSql, createTriggerSql, dispatchGroup, dispatchJobName, dropTriggerSql, outboxSql, quoteIdent, resourceTriggers, tableSql };
@@ -14,6 +14,8 @@ export type CapturedRow = {
14
14
  /** The columns an update changed, sensitive ones included; `null` for inserts and deletes. */
15
15
  changed: string[] | null;
16
16
  actor: EventActor | null;
17
+ /** When the writing transaction started, ISO 8601. */
18
+ at: string;
17
19
  };
18
20
  /**
19
21
  * One record's changes in one transaction, as one: its columns at the start (`before`, none when it was inserted) and
@@ -28,6 +30,8 @@ export type CoalescedChange = {
28
30
  actor: EventActor | null;
29
31
  /** The id of its last outbox row. */
30
32
  sequence: string;
33
+ /** When its last change's transaction started. */
34
+ at: string;
31
35
  };
32
36
  /**
33
37
  * The outbox rows of one or more transactions, in id order, as one change per (transaction, record). An insert then a
@@ -17,7 +17,8 @@ const merged = ({ first, last, actor, listed }) => {
17
17
  transaction: first.transaction,
18
18
  resource: first.resource,
19
19
  actor,
20
- sequence: last.id
20
+ sequence: last.id,
21
+ at: last.at
21
22
  };
22
23
  const before = first.op === "INSERT" ? null : first.old;
23
24
  const after = last.op === "DELETE" ? null : last.new;
@@ -0,0 +1,14 @@
1
+ import { Row } from "../types.js";
2
+ import { CapturedRow } from "./coalesce.js";
3
+ import { ResourceModel } from "@protobase/schema";
4
+ import { Db } from "@protobase/query";
5
+ //#region src/changes/outbox.d.ts
6
+ /**
7
+ * A captured row's columns from the JSON text the database stores: the columns of 64-bit integer and decimal fields as
8
+ * their exact digits, other numbers as numbers. Needs a runtime that passes JSON.parse revivers the source text
9
+ * (Node 21 and later, Bun).
10
+ */
11
+ export declare const parseColumns: (text: string | null, model?: ResourceModel) => Row | null;
12
+ /** The oldest `limit` rows of the outbox that committed, in id order. */
13
+ export declare const readOutbox: (db: Db, schema: string, models: readonly ResourceModel[], limit: number) => Promise<CapturedRow[]>;
14
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import { quoteIdent } from "./capture-sql.js";
2
+ import { sql } from "kysely";
3
+ //#region src/changes/outbox.ts
4
+ var ExactNumber = class {
5
+ source;
6
+ constructor(source) {
7
+ this.source = source;
8
+ }
9
+ };
10
+ const exact = (_key, value, context) => typeof value === "number" && context?.source !== void 0 ? new ExactNumber(context.source) : value;
11
+ const plain = (value) => {
12
+ if (value instanceof ExactNumber) return Number(value.source);
13
+ if (Array.isArray(value)) return value.map(plain);
14
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]));
15
+ return value;
16
+ };
17
+ const textTypes = /* @__PURE__ */ new Set(["bigint", "decimal"]);
18
+ /**
19
+ * A captured row's columns from the JSON text the database stores: the columns of 64-bit integer and decimal fields as
20
+ * their exact digits, other numbers as numbers. Needs a runtime that passes JSON.parse revivers the source text
21
+ * (Node 21 and later, Bun).
22
+ */
23
+ const parseColumns = (text, model) => {
24
+ if (text === null) return null;
25
+ const parsed = JSON.parse(text, exact);
26
+ const types = new Map(Object.values(model?.fields ?? {}).map((field) => [field.column, field.type]));
27
+ return Object.fromEntries(Object.entries(parsed).map(([column, value]) => [column, value instanceof ExactNumber && textTypes.has(types.get(column) ?? "") ? value.source : plain(value)]));
28
+ };
29
+ /** The oldest `limit` rows of the outbox that committed, in id order. */
30
+ const readOutbox = async (db, schema, models, limit) => {
31
+ const byName = new Map(models.map((model) => [model.name, model]));
32
+ const { rows } = await sql`select "id"::text as id, "transaction"::text as transaction, "resource", "op", "old"::text as old, "new"::text as new, "changed",
33
+ "actor"::text as actor, to_char("created_at" at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') as at
34
+ from ${sql.raw(`${quoteIdent(schema)}."outbox"`)} where "dispatch_after" <= now() order by "id" limit ${limit}`.execute(db);
35
+ return rows.map((row) => {
36
+ const model = byName.get(row.resource);
37
+ return {
38
+ ...row,
39
+ old: parseColumns(row.old, model),
40
+ new: parseColumns(row.new, model),
41
+ actor: row.actor === null ? null : JSON.parse(row.actor)
42
+ };
43
+ });
44
+ };
45
+ /** Removes dispatched rows from the outbox. */
46
+ const deleteOutbox = async (db, schema, ids) => {
47
+ if (ids.length === 0) return;
48
+ await sql`delete from ${sql.raw(`${quoteIdent(schema)}."outbox"`)} where "id" = any(${ids}::bigint[])`.execute(db);
49
+ };
50
+ //#endregion
51
+ export { deleteOutbox, parseColumns, readOutbox };
@@ -19,6 +19,8 @@ export type ResourceChange = {
19
19
  transaction: string;
20
20
  /** Grows with every change of a record, so receivers can drop stale events. */
21
21
  sequence: string;
22
+ /** When the change's transaction started, ISO 8601. */
23
+ at: string;
22
24
  };
23
25
  /** A captured row by field name, fields without a captured column left out. */
24
26
  export declare const recordOf: (model: ResourceModel, columns: Row) => Row;
@@ -36,7 +36,8 @@ const resourceChange = (model, change) => {
36
36
  source: change.actor ? "api" : "database",
37
37
  ...tenant != null && { tenant },
38
38
  transaction: change.transaction,
39
- sequence: change.sequence
39
+ sequence: change.sequence,
40
+ at: change.at
40
41
  };
41
42
  };
42
43
  //#endregion
@@ -1,12 +1,13 @@
1
1
  import { AdminAuth } from "./better-auth/create-auth.js";
2
2
  import { EventsOptions } from "./changes/events-options.js";
3
+ import { JobDefinition } from "./jobs/job-types.js";
4
+ import { Admin, Authenticator, PageSource, ViewSource } from "./types.js";
5
+ import { WebhooksOptions } from "./webhooks/table/options.js";
6
+ import { SubscriptionDefinition } from "./webhooks/subscription.js";
3
7
  import { FilesOptions } from "./files/options.js";
4
- import { AdminEnv, Authenticator, PageSource, ViewSource } from "./types.js";
5
8
  import { ResourceSource } from "./resource-source.js";
6
9
  import { FunctionSource } from "./functions/define-function.js";
7
10
  import { AdminOptions } from "./admin-options.js";
8
- import { JobDefinition } from "./jobs/job-types.js";
9
- import { Hono } from "hono";
10
11
  import { UserMenuSource } from "@protobase/schema";
11
12
  import { Db } from "@protobase/query";
12
13
  //#region src/create-admin.d.ts
@@ -27,10 +28,14 @@ export type CreateAdminInput = {
27
28
  files?: FilesOptions;
28
29
  /** Where the changes that resources' events are raised from are captured (see `EventsOptions`). */
29
30
  events?: EventsOptions;
30
- /** The app's jobs (see `defineJob`): with any, admins get the failed jobs page (`failedJobs`). */
31
+ /** The app's jobs (see `defineJob`): with any, or a resource with events, admins get the failed jobs page (`failedJobs`). */
31
32
  jobs?: JobDefinition[];
33
+ /** Where the resources' events are delivered (`subscription(...)` exports of the config module). */
34
+ subscriptions?: SubscriptionDefinition[];
35
+ /** Webhook subscriptions made in the admin app, from `extends: [webhooks()]` (see `WebhooksOptions`). */
36
+ webhooks?: WebhooksOptions;
32
37
  options?: AdminOptions;
33
38
  };
34
39
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
35
- export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, db, authenticate, auth, functions, files: filesOptions, events, jobs, options }: CreateAdminInput) => Hono<AdminEnv, import("hono/types").BlankSchema, "/">;
40
+ export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, db, authenticate, auth, functions, files: filesOptions, events, jobs, subscriptions, webhooks: webhooksOptions, options }: CreateAdminInput) => Admin;
36
41
  //#endregion
@@ -7,12 +7,15 @@ import { authBasePath } from "./better-auth/create-auth.js";
7
7
  import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
8
8
  import { statusRoute } from "./better-auth/status-route.js";
9
9
  import { changesSchema } from "./changes/events-options.js";
10
+ import { callerRecords } from "./functions/records.js";
11
+ import { toApiFunction } from "./functions/define-function.js";
12
+ import { createWebhooks } from "./webhooks/webhooks.js";
13
+ import { webhookTable } from "./webhooks/table/webhook-table.js";
14
+ import { subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
10
15
  import { checkShell } from "./check-shell.js";
11
16
  import { checkViews } from "./check-views.js";
12
17
  import { createFilesRuntime } from "./files/runtime.js";
13
18
  import { fileRoutes } from "./files/routes.js";
14
- import { toApiFunction } from "./functions/define-function.js";
15
- import { callerRecords } from "./functions/records.js";
16
19
  import { functionRoutes } from "./functions/routes.js";
17
20
  import { runtimeUrl } from "./runtime/runtime-settings.js";
18
21
  import { createMeta } from "./meta.js";
@@ -28,10 +31,11 @@ import { Hono } from "hono";
28
31
  import { checkPages } from "@protobase/layout";
29
32
  //#region src/create-admin.ts
30
33
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
31
- const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, db, authenticate, auth, functions = {}, files: filesOptions, events, jobs = [], options = {} }) => {
34
+ const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, db, authenticate, auth, functions = {}, files: filesOptions, events, jobs = [], subscriptions = [], webhooks: webhooksOptions, options = {} }) => {
32
35
  const basePath = options.basePath ?? "/api/v1";
33
- const resources = jobs.length > 0 ? [...appResources, failedJobs] : appResources;
34
- const views = jobs.length > 0 ? [...appViews, failedJobsView] : appViews;
36
+ const withJobs = jobs.length > 0 || appResources.some((resource) => resource.toModel().events);
37
+ const resources = withJobs ? [...appResources, failedJobs] : appResources;
38
+ const views = withJobs ? [...appViews, failedJobsView] : appViews;
35
39
  const registry = buildRegistry(resources);
36
40
  const apiFunctions = Object.fromEntries(Object.entries(functions).map(([name, source]) => [name, toApiFunction(name, source)]));
37
41
  const viewModels = views.map((view) => view.toModel());
@@ -52,6 +56,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
52
56
  models: registry.entries.map((entry) => entry.model),
53
57
  systemPath
54
58
  });
59
+ const table = webhooksOptions && webhookTable(webhooksOptions, auth);
55
60
  const deps = {
56
61
  db,
57
62
  registry,
@@ -62,7 +67,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
62
67
  systemPath,
63
68
  statementTimeoutMs: options.statementTimeoutMs ?? 15e3,
64
69
  guard: createScanGuard(db, options.scanGuard),
65
- hooks: options.writeHooks ?? [],
70
+ hooks: [...options.writeHooks ?? [], ...table ? [subscriptionWriteCheck(registry.entries.map((entry) => entry.model), table.checkUrl)] : []],
66
71
  audit: options.audit ?? consoleAuditQueue(),
67
72
  accessOptions: {
68
73
  ...options.roles && { roles: options.roles },
@@ -136,7 +141,15 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
136
141
  app.route("/", fileRoutes(deps, authenticate));
137
142
  app.route(basePath, api);
138
143
  app.route(deps.systemPath || "/", system);
139
- return app;
144
+ const webhooks = createWebhooks({
145
+ deps,
146
+ subscriptions,
147
+ functions: apiFunctions,
148
+ schema: changesSchema(events),
149
+ log: (line) => console.log(line),
150
+ ...table && { table }
151
+ });
152
+ return Object.assign(app, { jobs: webhooks.jobs });
140
153
  };
141
154
  //#endregion
142
155
  export { createAdmin };
package/dist/index.d.ts CHANGED
@@ -7,9 +7,13 @@ import { OrganizationsOptions } from "./better-auth/organizations/options.js";
7
7
  import { PlatformSignIn, readPlatformSignIn } from "./better-auth/platform-sign-in.js";
8
8
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
9
9
  import { EventsOptions } from "./changes/events-options.js";
10
+ import { StandardSchema, readJson } from "./functions/read-json.js";
11
+ import { EnqueueOptions, JobContext, JobDefinition, JobOptions } from "./jobs/job-types.js";
12
+ import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, SessionOrganization, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
13
+ import { WebhooksOptions } from "./webhooks/table/options.js";
14
+ import { EventData, RunContext, SubscriptionDefinition, SubscriptionEvent, SubscriptionOptions, subscription } from "./webhooks/subscription.js";
10
15
  import { FileProvider, FileStore, StoredObject } from "./files/store.js";
11
16
  import { FilesOptions } from "./files/options.js";
12
- import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, SessionOrganization, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
13
17
  import { CleanupEntry, CleanupSchedule, ScheduledDelete, storageCleanupSchedule } from "./files/cleanup-schedule.js";
14
18
  import { CleanupReport } from "./files/cleanup-runner.js";
15
19
  import { ResourceSource, configExports } from "./resource-source.js";
@@ -24,8 +28,6 @@ import { AssistantTool, ToolContext, defineTool } from "./assistant/tools.js";
24
28
  import { AssistantOptions, AssistantSettings, assistantRoles, assistantSettings, assistantVariables, seesAssistant } from "./assistant/assistant-settings.js";
25
29
  import { RuntimeOptions, runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
26
30
  import { AdminOptions } from "./admin-options.js";
27
- import { StandardSchema, readJson } from "./functions/read-json.js";
28
- import { EnqueueOptions, JobContext, JobDefinition, JobOptions } from "./jobs/job-types.js";
29
31
  import { CreateAdminInput, createAdmin } from "./create-admin.js";
30
32
  import { ProjectConfig, defineConfig, mergeConfig } from "./project-config.js";
31
33
  import { consoleAuditQueue } from "./audit/console-queue.js";
@@ -40,6 +42,10 @@ import { changesMigration, checkChanges, migrationCommand } from "./changes/capt
40
42
  import { CapturedRow, CoalescedChange, coalesce } from "./changes/coalesce.js";
41
43
  import { ResourceChange, recordOf, resourceChange } from "./changes/resource-change.js";
42
44
  import { eventMatcher } from "./changes/match-events.js";
45
+ import { parseColumns, readOutbox } from "./changes/outbox.js";
46
+ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
47
+ import { webhooks } from "./webhooks/table/plugin.js";
48
+ import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
43
49
  import { MemberInput, NewOrganization, addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
44
50
  import { AssistantBackend, assistantProtocolRoutes } from "./assistant/protocol-routes.js";
45
51
  import { fileConversationStore } from "./assistant/file-conversation-store.js";
@@ -60,4 +66,4 @@ import { FatalJobError } from "./jobs/run-job.js";
60
66
  import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
61
67
  import { jobsDrainApp } from "./jobs/drain-route.js";
62
68
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
63
- export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, normalizeRoles, notFound, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, unauthorized, verifySignature, verifyStandardWebhook, widget, withCacheBreakpoints };
69
+ export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webhooks, webhooksMigration, widget, withCacheBreakpoints };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.j
9
9
  import { requestApproval } from "./assistant/approval.js";
10
10
  import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
11
11
  import { ModelError, streamChatCompletion } from "./assistant/chat-completions.js";
12
+ import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
12
13
  import { runTurn } from "./assistant/turn.js";
13
14
  import { consoleAuditQueue } from "./audit/console-queue.js";
14
15
  import { readOperatorSettings } from "./better-auth/operator-provider.js";
@@ -18,23 +19,28 @@ import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
18
19
  import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
19
20
  import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
20
21
  import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
22
+ import { coalesce } from "./changes/coalesce.js";
23
+ import { eventMatcher } from "./changes/match-events.js";
24
+ import { parseColumns, readOutbox } from "./changes/outbox.js";
25
+ import { recordOf, resourceChange } from "./changes/resource-change.js";
26
+ import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
27
+ import { FatalJobError } from "./jobs/run-job.js";
28
+ import { defineFunction } from "./functions/define-function.js";
29
+ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
30
+ import { subscription } from "./webhooks/subscription.js";
21
31
  import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
22
32
  import { localFileStore, localFiles } from "./files/local-files.js";
23
- import { defineFunction } from "./functions/define-function.js";
24
33
  import { runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
25
- import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
26
34
  import { createAdmin } from "./create-admin.js";
27
35
  import { configExports } from "./resource-source.js";
28
- import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
29
36
  import { defineConfig, mergeConfig } from "./project-config.js";
30
37
  import { jwtAuthenticator } from "./auth/jwt.js";
31
38
  import { betterAuthAuthenticator } from "./better-auth/authenticator.js";
32
39
  import { issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
33
40
  import { authSchemaMigration, checkAuthSchema } from "./better-auth/auth-schema.js";
34
41
  import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
35
- import { coalesce } from "./changes/coalesce.js";
36
- import { recordOf, resourceChange } from "./changes/resource-change.js";
37
- import { eventMatcher } from "./changes/match-events.js";
42
+ import { webhooks } from "./webhooks/table/plugin.js";
43
+ import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
38
44
  import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
39
45
  import { functionApp } from "./functions/function-app.js";
40
46
  import { readJson } from "./functions/read-json.js";
@@ -42,7 +48,6 @@ import { verifySignature, verifyStandardWebhook } from "./functions/verify-signa
42
48
  import { requireEnv } from "./functions/require-env.js";
43
49
  import { createFileCleanup } from "./files/file-cleanup.js";
44
50
  import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
45
- import { FatalJobError } from "./jobs/run-job.js";
46
51
  import { startJobs } from "./jobs/runtime.js";
47
52
  import { jobsDrainApp } from "./jobs/drain-route.js";
48
- export { FatalJobError, HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, normalizeRoles, notFound, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, unauthorized, verifySignature, verifyStandardWebhook, widget, withCacheBreakpoints };
53
+ export { FatalJobError, HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webhooks, webhooksMigration, widget, withCacheBreakpoints };
@@ -4,5 +4,7 @@ import { Db } from "@protobase/query";
4
4
  /** What `defineJob(...).enqueue` reaches: the jobs runtime this process started. */
5
5
  export type JobsEnqueuer = {
6
6
  enqueue(definition: JobDefinition, db: Db, payload: unknown, options?: EnqueueOptions): Promise<string>;
7
+ /** Has this process's worker of the job look for due jobs now rather than at its next poll; nothing without one. */
8
+ wake(name: string): void;
7
9
  };
8
10
  //#endregion
@@ -7,6 +7,8 @@ const currentJobs = () => {
7
7
  if (!runtime) throw new Error("Jobs are not running in this process: start them with startJobs(), as protobase dev, serve and worker do");
8
8
  return runtime;
9
9
  };
10
+ /** The running jobs runtime, if this process started one. */
11
+ const runningJobs = () => holder[key];
10
12
  const setCurrentJobs = (runtime) => {
11
13
  if (holder[key]) throw new Error("Jobs are already running in this process; stop them before starting them again");
12
14
  holder[key] = runtime;
@@ -15,4 +17,4 @@ const clearCurrentJobs = (runtime) => {
15
17
  if (holder[key] === runtime) delete holder[key];
16
18
  };
17
19
  //#endregion
18
- export { clearCurrentJobs, currentJobs, setCurrentJobs };
20
+ export { clearCurrentJobs, currentJobs, runningJobs, setCurrentJobs };
@@ -1,5 +1,5 @@
1
- import { jobsSchema } from "./schema.js";
2
1
  import { clearCurrentJobs, setCurrentJobs } from "./current.js";
2
+ import { jobsSchema } from "./schema.js";
3
3
  import "./schedule.js";
4
4
  import { runJob, validatePayload } from "./run-job.js";
5
5
  import { jobsDatabase } from "./database.js";
@@ -98,10 +98,7 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
98
98
  };
99
99
  const id = options.key === void 0 ? await boss.send(name, data, send) : await keyed(options.key);
100
100
  if (!id) throw new Error(`Job "${name}" was not enqueued`);
101
- if (!target.isTransaction) {
102
- const worker = workers.get(name);
103
- if (worker) boss.notifyWorker(worker.id);
104
- }
101
+ if (!target.isTransaction) wake(name);
105
102
  return id;
106
103
  };
107
104
  const nextDue = async () => (await sql`select ${sql.id(jobsSchema, "next_due")}() as due`.execute(db)).rows[0].due;
@@ -129,9 +126,14 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
129
126
  await new Promise((resolve) => setTimeout(resolve, 100));
130
127
  }
131
128
  };
129
+ const wake = (name) => {
130
+ const worker = workers.get(name);
131
+ if (worker) boss.notifyWorker(worker.id);
132
+ };
132
133
  const runtime = {
133
134
  works: work,
134
135
  enqueue,
136
+ wake,
135
137
  update,
136
138
  drain,
137
139
  nextDue,
@@ -1,7 +1,8 @@
1
1
  import { AdminAuth } from "./better-auth/create-auth.js";
2
2
  import { EventsOptions } from "./changes/events-options.js";
3
- import { FilesOptions } from "./files/options.js";
4
3
  import { Authenticator } from "./types.js";
4
+ import { WebhooksOptions } from "./webhooks/table/options.js";
5
+ import { FilesOptions } from "./files/options.js";
5
6
  import { FunctionSource } from "./functions/define-function.js";
6
7
  import { AdminOptions } from "./admin-options.js";
7
8
  import { Db } from "@protobase/query";
@@ -24,6 +25,8 @@ export type ProjectConfig = {
24
25
  files?: FilesOptions;
25
26
  /** Where the changes that resources' events are raised from are captured (see `EventsOptions`). */
26
27
  events?: EventsOptions;
28
+ /** Webhook subscriptions made in the admin app; set by `extends: [webhooks()]` (see `WebhooksOptions`). */
29
+ webhooks?: WebhooksOptions;
27
30
  /** The module of the app's jobs, e.g. `import * as jobs from './config/jobs'`: every `defineJob` it exports. */
28
31
  jobs?: Record<string, unknown>;
29
32
  options?: AdminOptions;
@@ -1,4 +1,5 @@
1
1
  import { jobDefinitions } from "./jobs/define-job.js";
2
+ import { isSubscription } from "./webhooks/subscription.js";
2
3
  import { isPageDefinition } from "@protobase/layout";
3
4
  //#region src/project-config.ts
4
5
  /** Typed identity, for `export default defineConfig({ extends: [...], config, auth, ... })` in `protobase.config.ts`. */
@@ -41,6 +42,11 @@ const mergedModule = (named) => {
41
42
  value,
42
43
  label
43
44
  })));
45
+ checkUnique("The subscription", exports.filter(({ value }) => isSubscription(value)).map(({ value, label }) => ({
46
+ name: value.name,
47
+ value,
48
+ label
49
+ })));
44
50
  const menus = values.filter(({ value }) => typeof value?.toUserMenuModel === "function");
45
51
  if (new Set(menus.map(({ value }) => value)).size > 1) throw new Error(`Only one config may export a user menu; ${menus.map(({ label }) => label).join(" and ")} each do`);
46
52
  const module = {};
@@ -133,11 +139,12 @@ const mergedOptions = (named) => {
133
139
  */
134
140
  const mergeConfig = (...configs) => {
135
141
  const named = configs.flatMap((config, index) => flatten(config, config.name ?? `config ${index + 1}`));
136
- const { db, authenticate, auth, events } = lastSet(named.map(({ config }) => ({
142
+ const { db, authenticate, auth, events, webhooks } = lastSet(named.map(({ config }) => ({
137
143
  db: config.db,
138
144
  authenticate: config.authenticate,
139
145
  auth: config.auth,
140
- events: config.events
146
+ events: config.events,
147
+ webhooks: config.webhooks
141
148
  })));
142
149
  const module = mergedModule(named);
143
150
  const functions = mergedFunctions(named);
@@ -154,6 +161,7 @@ const mergeConfig = (...configs) => {
154
161
  ...functions && { functions },
155
162
  ...files && { files },
156
163
  ...events && { events },
164
+ ...webhooks && { webhooks },
157
165
  ...jobs && { jobs },
158
166
  ...options && { options }
159
167
  };
@@ -1,5 +1,7 @@
1
+ import { SubscriptionDefinition } from "./webhooks/subscription.js";
2
+ import "./index.js";
1
3
  import { PageDefinition } from "@protobase/layout";
2
- import { AccessSource, FileProcessor, UserMenuSource, ViewModel } from "@protobase/schema";
4
+ import { AccessSource, EventMatch, FileProcessor, UserMenuSource, ViewModel } from "@protobase/schema";
3
5
  import { z } from "zod";
4
6
  //#region src/resource-source.d.ts
5
7
  type FieldSource = {
@@ -22,10 +24,12 @@ export type ResourceSource = AccessSource & {
22
24
  message: string;
23
25
  }> | undefined)[];
24
26
  recordSchema(): z.ZodType;
27
+ /** The code predicates of its events, by event name. */
28
+ eventMatches?(): Record<string, EventMatch<any>>;
25
29
  };
26
30
  /**
27
- * Splits a config module (`import * as config from './config'`) into its resources, views, pages and, when it exports
28
- * one `userMenu(...)`, the user menu.
31
+ * Splits a config module (`import * as config from './config'`) into its resources, views, pages, subscriptions and,
32
+ * when it exports one `userMenu(...)`, the user menu.
29
33
  */
30
34
  export declare const configExports: (module: Record<string, unknown>) => {
31
35
  userMenu?: UserMenuSource | undefined;
@@ -34,5 +38,6 @@ export declare const configExports: (module: Record<string, unknown>) => {
34
38
  toModel(): ViewModel;
35
39
  }>;
36
40
  pages: PageDefinition[];
41
+ subscriptions: SubscriptionDefinition[];
37
42
  };
38
43
  //#endregion
@@ -1,13 +1,15 @@
1
+ import { isSubscription } from "./webhooks/subscription.js";
1
2
  import { isPageDefinition } from "@protobase/layout";
2
3
  //#region src/resource-source.ts
3
4
  /**
4
- * Splits a config module (`import * as config from './config'`) into its resources, views, pages and, when it exports
5
- * one `userMenu(...)`, the user menu.
5
+ * Splits a config module (`import * as config from './config'`) into its resources, views, pages, subscriptions and,
6
+ * when it exports one `userMenu(...)`, the user menu.
6
7
  */
7
8
  const configExports = (module) => {
8
9
  const values = Object.values(module);
9
10
  const pages = values.filter(isPageDefinition);
10
- const others = values.filter((value) => !isPageDefinition(value));
11
+ const subscriptions = [...new Set(values.filter(isSubscription))];
12
+ const others = values.filter((value) => !isPageDefinition(value) && !isSubscription(value));
11
13
  const buildable = others.filter((value) => typeof value?.toModel === "function");
12
14
  const menus = others.filter((value) => typeof value?.toUserMenuModel === "function");
13
15
  if (menus.length > 1) throw new Error(`The config module exports ${menus.length} user menus; export one userMenu(...)`);
@@ -15,6 +17,7 @@ const configExports = (module) => {
15
17
  resources: buildable.filter((value) => typeof value.recordSchema === "function"),
16
18
  views: buildable.filter((value) => typeof value.recordSchema !== "function"),
17
19
  pages,
20
+ subscriptions,
18
21
  ...menus[0] && { userMenu: menus[0] }
19
22
  };
20
23
  };
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { JobDefinition } from "./jobs/job-types.js";
1
2
  import { Hono } from "hono";
2
3
  import { PageModel } from "@protobase/layout";
3
4
  import { ResourceModel, ViewModel } from "@protobase/schema";
@@ -88,5 +89,8 @@ export type ViewSource = {
88
89
  export type PageSource = {
89
90
  toModel(): PageModel;
90
91
  };
91
- export type Admin = Hono<AdminEnv>;
92
+ /** The admin API, and the jobs it needs a worker to run: the delivery of resource events. */
93
+ export type Admin = Hono<AdminEnv> & {
94
+ jobs: JobDefinition[];
95
+ };
92
96
  //#endregion