bunderstack 0.19.1 → 0.20.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/CHANGELOG.md +32 -0
  2. package/dist/api/crud-router.d.ts +19 -24
  3. package/dist/api/crud-router.d.ts.map +1 -1
  4. package/dist/api/crud-router.js +39 -18
  5. package/dist/api/crud-router.js.map +1 -1
  6. package/dist/api/live-window.d.ts.map +1 -1
  7. package/dist/api/live-window.js +13 -3
  8. package/dist/api/live-window.js.map +1 -1
  9. package/dist/api/realtime-router.d.ts +2 -0
  10. package/dist/api/realtime-router.d.ts.map +1 -1
  11. package/dist/api/realtime-router.js +1 -0
  12. package/dist/api/realtime-router.js.map +1 -1
  13. package/dist/api/types.d.ts +2 -2
  14. package/dist/api/types.d.ts.map +1 -1
  15. package/dist/api/types.js.map +1 -1
  16. package/dist/blueprint-generator.d.ts.map +1 -1
  17. package/dist/blueprint-generator.js +8 -2
  18. package/dist/blueprint-generator.js.map +1 -1
  19. package/dist/blueprint.d.ts +3 -1
  20. package/dist/blueprint.d.ts.map +1 -1
  21. package/dist/blueprint.js +2 -2
  22. package/dist/blueprint.js.map +1 -1
  23. package/dist/codegen.d.ts +36 -0
  24. package/dist/codegen.d.ts.map +1 -0
  25. package/dist/codegen.js +235 -0
  26. package/dist/codegen.js.map +1 -0
  27. package/dist/crud-operations.d.ts.map +1 -1
  28. package/dist/crud-operations.js +7 -3
  29. package/dist/crud-operations.js.map +1 -1
  30. package/dist/index.d.ts +9 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/live/protocol.d.ts +3 -0
  34. package/dist/live/protocol.d.ts.map +1 -1
  35. package/dist/live/protocol.js.map +1 -1
  36. package/dist/realtime/facade.d.ts +3 -1
  37. package/dist/realtime/facade.d.ts.map +1 -1
  38. package/dist/realtime/facade.js +2 -1
  39. package/dist/realtime/facade.js.map +1 -1
  40. package/dist/realtime/filter.d.ts.map +1 -1
  41. package/dist/realtime/filter.js +1 -0
  42. package/dist/realtime/filter.js.map +1 -1
  43. package/dist/realtime/publisher.d.ts +2 -0
  44. package/dist/realtime/publisher.d.ts.map +1 -1
  45. package/dist/realtime/publisher.js.map +1 -1
  46. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to `bunderstack` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.20.0] - 2026-08-25
8
+
9
+ ### Added
10
+
11
+ - **Framework-neutral typed client.** The new `bunderstack-client` package
12
+ exposes `createClient<App>()`, confirmed realtime `LiveView`, and thin Solid,
13
+ React, Vue, and Svelte adapters. TanStack Query and TanStack DB integrations
14
+ reuse the same transport and realtime primitives.
15
+ - **Confirmed mutation correlation.** CRUD writes propagate an opaque
16
+ `operationId` into live-view frames. Reconnect snapshots settle successful
17
+ writes whose delta was lost, and a heartbeat watchdog reconnects silent
18
+ streams.
19
+
20
+ ### Changed
21
+
22
+ - **Direct oRPC CRUD inputs.** Update procedures now accept
23
+ `{ id, ...changes }`; REST keeps `PATCH /api/{table}/{id}` and rejects an
24
+ immutable `id` in the body. Live frames preserve the table row type.
25
+ - **Optional REST artifact.** OpenAPI route-map generation is now the fallback
26
+ for separate frontend repositories and deliberately excludes Better Auth
27
+ routes, which continue to use the official Better Auth client.
28
+
29
+ ## [0.19.2] - 2026-08-24
30
+
31
+ ### Added
32
+
33
+ - **Solid and Bun SSR application support in deployment blueprint.** The
34
+ `bunderstack blueprint` generator and contract schema now support
35
+ standalone Solid 2, Bun SSR, and custom full-stack setups without requiring
36
+ `@tanstack/react-start`. The generator automatically infers the framework from
37
+ declared dependencies.
38
+
7
39
  ## [0.19.1] - 2026-08-23
8
40
 
9
41
  ### Fixed
@@ -20,7 +20,6 @@ export type CrudApiRouterOptions<TSchema extends Record<string, unknown> = Recor
20
20
  */
21
21
  livePublisher?: RealtimePublisher;
22
22
  };
23
- type CrudUpdate<TTable extends Table> = Partial<Omit<TTable['$inferInsert'], 'id'>>;
24
23
  /** A filter accepts one value (`=`), a list (`IN`), or null (`IS NULL`). */
25
24
  export type ListFilterValue<T> = T | readonly T[] | null | 'null';
26
25
  export type ListFilters<TTable extends Table, TFilterable extends string> = {
@@ -249,14 +248,11 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
249
248
  }>;
250
249
  };
251
250
  }, Extract<TTable["$inferSelect"], import("@orpc/server").AnyORPCError>>;
252
- update: import("@orpc/server").DecoratedProcedure<import("./context.js").ApiContext<TSchema, Record<string, unknown>> & object, object, v.StrictObjectSchema<{
253
- readonly params: v.StrictObjectSchema<{
254
- readonly id: v.StringSchema<undefined>;
255
- }, undefined>;
256
- readonly query: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, {}>;
257
- readonly headers: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, {}>;
258
- readonly body: v.GenericSchema<CrudUpdate<TTable>, CrudUpdate<TTable>>;
259
- }, undefined>, v.GenericSchema<TTable["$inferSelect"], TTable["$inferSelect"]>, {
251
+ update: import("@orpc/server").DecoratedProcedure<import("./context.js").ApiContext<TSchema, Record<string, unknown>> & object, object, v.GenericSchema<{
252
+ id: string;
253
+ } & Partial<Omit<TTable["$inferInsert"], "id">>, {
254
+ id: string;
255
+ } & Partial<Omit<TTable["$inferInsert"], "id">>>, v.GenericSchema<TTable["$inferSelect"], TTable["$inferSelect"]>, {
260
256
  readonly BAD_REQUEST: {
261
257
  readonly data: v.OptionalSchema<v.StrictObjectSchema<{
262
258
  readonly code: v.OptionalSchema<v.LiteralSchema<"BAD_REQUEST", undefined>, "BAD_REQUEST">;
@@ -376,43 +372,41 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
376
372
  }, never>;
377
373
  live: import("@orpc/server").DecoratedProcedure<import("./context.js").ApiContext<TSchema, Record<string, unknown>> & object, object, v.GenericSchema<LiveInputFor<TTable, TFilterable, TSortable>, LiveInputFor<TTable, TFilterable, TSortable>>, import("@orpc/contract").Schema<AsyncIteratorObject<{
378
374
  type: "snapshot";
379
- items: {
380
- [x: string]: unknown;
381
- }[];
375
+ items: TTable["$inferSelect"][];
382
376
  sort: string;
383
377
  order: "asc" | "desc";
384
378
  limit: number;
385
379
  hasMore: boolean;
380
+ operationId?: string | undefined;
386
381
  } | {
387
382
  type: "upsert";
388
- record: {
389
- [x: string]: unknown;
390
- };
383
+ record: TTable["$inferSelect"];
391
384
  afterId: string | null;
385
+ operationId?: string | undefined;
392
386
  } | {
393
387
  type: "remove";
394
388
  id: string;
389
+ operationId?: string | undefined;
395
390
  } | {
396
391
  type: "heartbeat";
397
392
  intervalMs: number;
398
393
  }, unknown, void>, import("@orpc/server").AsyncIteratorClass<{
399
394
  type: "snapshot";
400
- items: {
401
- [x: string]: unknown;
402
- }[];
395
+ items: TTable["$inferSelect"][];
403
396
  sort: string;
404
397
  order: "asc" | "desc";
405
398
  limit: number;
406
399
  hasMore: boolean;
400
+ operationId?: string | undefined;
407
401
  } | {
408
402
  type: "upsert";
409
- record: {
410
- [x: string]: unknown;
411
- };
403
+ record: TTable["$inferSelect"];
412
404
  afterId: string | null;
405
+ operationId?: string | undefined;
413
406
  } | {
414
407
  type: "remove";
415
408
  id: string;
409
+ operationId?: string | undefined;
416
410
  } | {
417
411
  type: "heartbeat";
418
412
  intervalMs: number;
@@ -475,7 +469,8 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
475
469
  };
476
470
  }, never>;
477
471
  };
478
- export type TableCrudProcedures<TTable extends Table, TFilterable extends string = string, TSortable extends string = string> = ReturnType<typeof buildTableCrudProcedures<Record<string, unknown>, TTable, TFilterable, TSortable>>;
479
- export declare function buildCrudApiRouter<TSchema extends Record<string, unknown>, TAccess extends Record<string, TableAccessInput> | undefined = undefined>(schema: TSchema, db: AnyDb, options: CrudApiRouterOptions<TSchema>): CrudApiRouterFor<TSchema, TAccess>;
480
- export {};
472
+ export type TableCrudProcedures<TTable extends Table, TFilterable extends string = string, TSortable extends string = string, TLive extends boolean = false> = Omit<ReturnType<typeof buildTableCrudProcedures<Record<string, unknown>, TTable, TFilterable, TSortable>>, 'live'> & (TLive extends true ? {
473
+ live: ReturnType<typeof buildTableCrudProcedures<Record<string, unknown>, TTable, TFilterable, TSortable>>['live'];
474
+ } : {});
475
+ export declare function buildCrudApiRouter<TSchema extends Record<string, unknown>, TAccess extends Record<string, TableAccessInput> | undefined = undefined, TLive extends boolean = false>(schema: TSchema, db: AnyDb, options: CrudApiRouterOptions<TSchema>): CrudApiRouterFor<TSchema, TAccess, TLive>;
481
476
  //# sourceMappingURL=crud-router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crud-router.d.ts","sourceRoot":"","sources":["../../src/api/crud-router.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"crud-router.d.ts","sourceRoot":"","sources":["../../src/api/crud-router.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,16 @@
1
+ import { eventIterator } from '@orpc/server';
1
2
  import { getTableColumns, getTableName, isTable } from 'drizzle-orm';
2
- import { createInsertSchema, createSelectSchema, createUpdateSchema, } from 'drizzle-valibot';
3
3
  import '@orpc/openapi/extensions/route';
4
- import { eventIterator } from '@orpc/server';
4
+ import { createInsertSchema, createSelectSchema, createUpdateSchema, } from 'drizzle-valibot';
5
5
  import * as v from 'valibot';
6
6
  import { tableEntryForName, } from '../access.js';
7
7
  import { createCrudOperations } from '../crud-operations.js';
8
+ import { BunderstackError } from '../errors.js';
8
9
  import { filterTableChanges } from '../realtime/filter.js';
9
10
  import { REALTIME_HEARTBEAT_INTERVAL_MS, withRealtimeHeartbeat, } from '../realtime/heartbeat.js';
10
11
  import { createApiBuilder } from './builder.js';
11
- import { createLiveWindow } from './live-window.js';
12
12
  import { buildListInputSchema, buildLiveInputSchema } from './list-input-schema.js';
13
+ import { createLiveWindow } from './live-window.js';
13
14
  function strictObject(schema) {
14
15
  return v.strictObject(schema.entries);
15
16
  }
@@ -47,13 +48,12 @@ export function buildTableCrudProcedures(args) {
47
48
  const updateBodySchema = v.omit(generatedUpdateSchema, [
48
49
  'id',
49
50
  ]);
51
+ // Compact input is transport-neutral: direct oRPC callers pass
52
+ // `{ id, ...changes }`, while OpenAPI still maps `id` to the path and the
53
+ // remaining fields to the PATCH body.
50
54
  const updateInputSchema = v.strictObject({
51
- params: v.strictObject({ id: v.string() }),
52
- query: v.optional(v.record(v.string(), v.unknown()), {}),
53
- headers: v.optional(v.record(v.string(), v.unknown()), {}),
54
- // Same reason as `selectSchema`: state the type instead of letting
55
- // declaration emit inline drizzle-valibot's generics.
56
- body: strictObject(updateBodySchema),
55
+ id: v.string(),
56
+ ...updateBodySchema.entries,
57
57
  });
58
58
  // One filter field per allowed column, typed by the column itself: a scalar
59
59
  // for `=`, a list for `IN`, and `null` for `IS NULL`. Query strings are
@@ -155,18 +155,28 @@ export function buildTableCrudProcedures(args) {
155
155
  path: `/api/${name}/{id}`,
156
156
  summary: `Update ${name}`,
157
157
  tags: [name],
158
- inputStructure: 'detailed',
159
158
  })
160
159
  .input(updateInputSchema)
161
160
  .output(selectSchema)
162
161
  .handler(async ({ input, context }) => {
162
+ const requestPath = new URL(context.request.url).pathname;
163
+ if (!requestPath.startsWith('/api/rpc')) {
164
+ const rawBody = await context.getRawBody();
165
+ if (rawBody) {
166
+ const body = JSON.parse(rawBody);
167
+ if (Object.hasOwn(body, 'id')) {
168
+ throw new BunderstackError('BAD_REQUEST', 'The id field is immutable');
169
+ }
170
+ }
171
+ }
163
172
  const session = await context.getSession();
164
173
  const execCtx = {
165
174
  request: context.request,
166
175
  user: session.user,
167
176
  session: { activeOrganizationId: session.activeOrganizationId },
168
177
  };
169
- return (await operations.update(name, input.params.id, input.body, execCtx));
178
+ const { id, ...changes } = input;
179
+ return (await operations.update(name, id, changes, execCtx));
170
180
  });
171
181
  // 5. DELETE procedure
172
182
  const deleteProc = builder.public
@@ -197,22 +207,27 @@ export function buildTableCrudProcedures(args) {
197
207
  filterableColumns: access.filterableColumns,
198
208
  sortableColumns: access.sortableColumns,
199
209
  });
200
- const recordSchema = v.record(v.string(), v.unknown());
201
210
  const liveFrameSchema = v.union([
202
211
  v.strictObject({
203
212
  type: v.literal('snapshot'),
204
- items: v.array(recordSchema),
213
+ items: v.array(selectSchema),
205
214
  sort: v.string(),
206
215
  order: v.picklist(['asc', 'desc']),
207
216
  limit: v.number(),
208
217
  hasMore: v.boolean(),
218
+ operationId: v.optional(v.string()),
209
219
  }),
210
220
  v.strictObject({
211
221
  type: v.literal('upsert'),
212
- record: recordSchema,
222
+ record: selectSchema,
213
223
  afterId: v.union([v.string(), v.null()]),
224
+ operationId: v.optional(v.string()),
225
+ }),
226
+ v.strictObject({
227
+ type: v.literal('remove'),
228
+ id: v.string(),
229
+ operationId: v.optional(v.string()),
214
230
  }),
215
- v.strictObject({ type: v.literal('remove'), id: v.string() }),
216
231
  v.strictObject({ type: v.literal('heartbeat'), intervalMs: v.number() }),
217
232
  ]);
218
233
  const live = builder.public
@@ -252,7 +267,7 @@ export function buildTableCrudProcedures(args) {
252
267
  session: { activeOrganizationId: session.activeOrganizationId },
253
268
  };
254
269
  let view;
255
- const readSnapshot = async () => {
270
+ const readSnapshot = async (operationId) => {
256
271
  const result = await operations.list(name, input ?? {}, execCtx);
257
272
  view = createLiveWindow({
258
273
  sort: result.sort,
@@ -268,15 +283,21 @@ export function buildTableCrudProcedures(args) {
268
283
  order: result.order,
269
284
  limit: result.limit,
270
285
  hasMore: result.hasMore,
286
+ ...(operationId ? { operationId } : {}),
271
287
  };
272
288
  };
273
289
  yield await readSnapshot();
274
290
  for await (const change of changes) {
275
291
  const outcome = view.apply(change);
276
- if (outcome.type === 'none')
292
+ if (outcome.type === 'none') {
293
+ // The write succeeded but did not affect this view. A snapshot
294
+ // still acknowledges the operation without inventing a row.
295
+ if (change.operationId)
296
+ yield await readSnapshot(change.operationId);
277
297
  continue;
298
+ }
278
299
  if (outcome.type === 'resnapshot') {
279
- yield await readSnapshot();
300
+ yield await readSnapshot(change.operationId);
280
301
  continue;
281
302
  }
282
303
  for (const frame of outcome.frames)
@@ -1 +1 @@
1
- {"version":3,"file":"crud-router.js","sourceRoot":"","sources":["../../src/api/crud-router.ts"],"names":[],"mappings":"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","sourcesContent":["import { getTableColumns, getTableName, isTable, type Table } from 'drizzle-orm'\nimport {\n createInsertSchema,\n createSelectSchema,\n createUpdateSchema,\n} from 'drizzle-valibot'\nimport '@orpc/openapi/extensions/route'\nimport { eventIterator } from '@orpc/server'\nimport * as v from 'valibot'\n\nimport type { AnyDb } from '../dialect'\nimport type { IdempotencyConfig } from '../idempotency'\nimport type { LiveSnapshotFrame } from '../live/protocol'\nimport type { RealtimeFacade } from '../realtime/facade'\nimport type { RealtimePublisher } from '../realtime/publisher'\nimport type { CrudApiRouterFor } from './types'\n\nimport {\n tableEntryForName,\n type ResolvedAccess,\n type ResolvedTableAccess,\n type TableAccessInput,\n} from '../access'\nimport { createCrudOperations, type CrudOperations } from '../crud-operations'\nimport { filterTableChanges } from '../realtime/filter'\nimport {\n REALTIME_HEARTBEAT_INTERVAL_MS,\n withRealtimeHeartbeat,\n} from '../realtime/heartbeat'\nimport { createApiBuilder } from './builder'\nimport { createLiveWindow } from './live-window'\nimport { buildListInputSchema, buildLiveInputSchema } from './list-input-schema'\n\nexport type CrudApiRouterOptions<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n> = {\n access: ResolvedAccess\n idempotency?: boolean | IdempotencyConfig\n realtime?: RealtimeFacade<TSchema>\n /**\n * The raw realtime publisher, present when realtime is enabled. The CRUD\n * router subscribes on behalf of live views (`GET /{table}:live`);\n * publishing stays behind the facade.\n */\n livePublisher?: RealtimePublisher\n}\n\nfunction strictObject<TEntries extends v.ObjectEntries>(schema: {\n entries: TEntries\n}) {\n return v.strictObject(schema.entries)\n}\n\ntype CrudInsert<TTable extends Table> = Partial<TTable['$inferInsert']>\ntype CrudUpdate<TTable extends Table> = Partial<\n Omit<TTable['$inferInsert'], 'id'>\n>\n\n/** A filter accepts one value (`=`), a list (`IN`), or null (`IS NULL`). */\nexport type ListFilterValue<T> = T | readonly T[] | null | 'null'\n\nexport type ListFilters<TTable extends Table, TFilterable extends string> = {\n [K in Extract<keyof TTable['$inferSelect'], TFilterable>]?: ListFilterValue<\n TTable['$inferSelect'][K]\n >\n}\n\n/**\n * The `list` input as callers see it. Filter columns and sortable columns come\n * from the table's `access` entry, so `filters` autocompletes to real columns\n * and rejects wrong value types at compile time.\n */\nexport type ListInputFor<\n TTable extends Table,\n TFilterable extends string,\n TSortable extends string,\n> =\n | {\n limit?: number\n offset?: number\n cursor?: string\n sort?: TSortable\n order?: 'asc' | 'desc'\n q?: string\n count?: boolean\n filters?: ListFilters<TTable, TFilterable>\n }\n | undefined\n\n/**\n * The live-view input as callers see it: the list contract narrowed to what a\n * stream can honor — no text search, no pagination.\n */\nexport type LiveInputFor<\n TTable extends Table,\n TFilterable extends string,\n TSortable extends string,\n> =\n | {\n limit?: number\n sort?: TSortable\n order?: 'asc' | 'desc'\n filters?: ListFilters<TTable, TFilterable>\n }\n | undefined\n\nexport type BuildTableCrudProceduresArgs<\n TSchema extends Record<string, unknown>,\n TTable extends Table,\n> = {\n table: TTable\n operations: CrudOperations\n builder: ReturnType<typeof createApiBuilder<TSchema>>\n access: ResolvedTableAccess\n /** The schema key events are published under. */\n schemaKey: string\n /** Present when realtime is enabled; without it no live procedure is built. */\n livePublisher?: RealtimePublisher\n}\n\nexport function buildTableCrudProcedures<\n TSchema extends Record<string, unknown>,\n TTable extends Table,\n TFilterable extends string = string,\n TSortable extends string = string,\n>(args: BuildTableCrudProceduresArgs<TSchema, TTable>) {\n const { table, operations, builder, access, schemaKey, livePublisher } =\n args\n const name = getTableName(table)\n\n // drizzle-valibot infers its schemas through internal generics. Left as-is,\n // declaration emit inlines those generics by name into the published `.d.ts`,\n // where they are unbound — every column silently becomes optional for the\n // consumer. Restating the type keeps the runtime schema and makes the emitted\n // one both correct and readable.\n const selectSchema = strictObject(\n createSelectSchema(table),\n ) as unknown as v.GenericSchema<\n TTable['$inferSelect'],\n TTable['$inferSelect']\n >\n const generatedInsertSchema = strictObject(createInsertSchema(table))\n const generatedColumns = Object.keys(generatedInsertSchema.entries)\n const columns = getTableColumns(table)\n const serverManagedColumns = [\n ...access.readonlyColumns,\n ...(access.writeScope && generatedColumns.includes('organizationId')\n ? ['organizationId']\n : []),\n ...Object.entries(columns)\n .filter(([, column]) => !column.notNull || column.hasDefault)\n .map(([column]) => column),\n ].filter((column) => generatedColumns.includes(column))\n const insertEntries: Record<\n string,\n v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>\n > = {\n ...generatedInsertSchema.entries,\n }\n for (const column of serverManagedColumns) {\n const schema = insertEntries[column]\n if (schema) insertEntries[column] = v.optional(schema)\n }\n const insertSchema = v.strictObject(\n insertEntries,\n ) as unknown as v.GenericSchema<CrudInsert<TTable>, CrudInsert<TTable>>\n const generatedUpdateSchema = createUpdateSchema(table)\n const updateBodySchema = v.omit(generatedUpdateSchema, [\n 'id' as keyof typeof generatedUpdateSchema.entries,\n ])\n const updateInputSchema = v.strictObject({\n params: v.strictObject({ id: v.string() }),\n query: v.optional(v.record(v.string(), v.unknown()), {}),\n headers: v.optional(v.record(v.string(), v.unknown()), {}),\n // Same reason as `selectSchema`: state the type instead of letting\n // declaration emit inline drizzle-valibot's generics.\n body: strictObject(updateBodySchema) as unknown as v.GenericSchema<\n CrudUpdate<TTable>,\n CrudUpdate<TTable>\n >,\n })\n\n // One filter field per allowed column, typed by the column itself: a scalar\n // for `=`, a list for `IN`, and `null` for `IS NULL`. Query strings are\n // coerced to these types by SmartCoercionHandlerPlugin, so REST and RPC share\n // one contract and nothing has to re-read the raw URL.\n // The cast restates the schema with the literals the caller's `access`\n // config carries, which the runtime column lists cannot name on their own.\n const listQuerySchema = buildListInputSchema(table, {\n filterableColumns: access.filterableColumns,\n sortableColumns: access.sortableColumns,\n }) as unknown as v.GenericSchema<\n ListInputFor<TTable, TFilterable, TSortable>,\n ListInputFor<TTable, TFilterable, TSortable>\n >\n\n const listOutputSchema = v.strictObject({\n items: v.array(selectSchema),\n nextCursor: v.optional(v.string()),\n hasMore: v.boolean(),\n total: v.optional(v.number()),\n limit: v.optional(v.number()),\n offset: v.optional(v.number()),\n cursor: v.optional(v.string()),\n q: v.optional(v.string()),\n sort: v.optional(v.string()),\n order: v.optional(v.string()),\n })\n\n // 1. LIST procedure\n const list = builder.public\n .route({\n method: 'GET',\n path: `/api/${name}`,\n summary: `List ${name}`,\n tags: [name],\n })\n .input(listQuerySchema)\n .output(listOutputSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n const result = await operations.list(name, input ?? {}, execCtx)\n return {\n ...result,\n items: result.items as TTable['$inferSelect'][],\n }\n })\n\n // 2. GET procedure\n const get = builder.public\n .route({\n method: 'GET',\n path: `/api/${name}/{id}`,\n summary: `Get ${name} by ID`,\n tags: [name],\n })\n .input(v.strictObject({ id: v.string() }))\n .output(selectSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n return (await operations.get(\n name,\n input.id,\n execCtx,\n )) as TTable['$inferSelect']\n })\n\n // 3. CREATE procedure\n const create = builder.public\n .route({\n method: 'POST',\n path: `/api/${name}`,\n summary: `Create ${name}`,\n tags: [name],\n successStatus: 201,\n })\n .input(insertSchema)\n .output(selectSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n const idempotencyKey = context.request.headers\n .get('Idempotency-Key')\n ?.trim()\n const rawBody = await context.getRawBody()\n\n const res = await operations.create(\n name,\n input,\n rawBody,\n idempotencyKey,\n execCtx,\n )\n if (res.type === 'replay') {\n context.resHeaders.set('Idempotency-Replayed', 'true')\n return res.record as TTable['$inferSelect']\n }\n return res.record as TTable['$inferSelect']\n })\n\n // 4. UPDATE procedure\n const update = builder.public\n .route({\n method: 'PATCH',\n path: `/api/${name}/{id}`,\n summary: `Update ${name}`,\n tags: [name],\n inputStructure: 'detailed',\n })\n .input(updateInputSchema)\n .output(selectSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n\n return (await operations.update(\n name,\n input.params.id,\n input.body,\n execCtx,\n )) as TTable['$inferSelect']\n })\n\n // 5. DELETE procedure\n const deleteProc = builder.public\n .route({\n method: 'DELETE',\n path: `/api/${name}/{id}`,\n summary: `Delete ${name}`,\n tags: [name],\n successStatus: 204,\n })\n .input(v.strictObject({ id: v.string() }))\n .output(v.undefined())\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n await operations.delete(name, input.id, execCtx)\n return undefined\n })\n\n // 6. LIVE procedure — one list query as a stream. A snapshot first, then the\n // changes this view cares about, placed by the server. Every connection\n // opens with a snapshot, so a reconnect is its own recovery: no client-side\n // event buffer, no Last-Event-ID bookkeeping, and no refetch path.\n const liveQuerySchema = buildLiveInputSchema(table, {\n filterableColumns: access.filterableColumns,\n sortableColumns: access.sortableColumns,\n }) as unknown as v.GenericSchema<\n LiveInputFor<TTable, TFilterable, TSortable>,\n LiveInputFor<TTable, TFilterable, TSortable>\n >\n\n const recordSchema = v.record(v.string(), v.unknown())\n const liveFrameSchema = v.union([\n v.strictObject({\n type: v.literal('snapshot'),\n items: v.array(recordSchema),\n sort: v.string(),\n order: v.picklist(['asc', 'desc']),\n limit: v.number(),\n hasMore: v.boolean(),\n }),\n v.strictObject({\n type: v.literal('upsert'),\n record: recordSchema,\n afterId: v.union([v.string(), v.null()]),\n }),\n v.strictObject({ type: v.literal('remove'), id: v.string() }),\n v.strictObject({ type: v.literal('heartbeat'), intervalMs: v.number() }),\n ])\n\n const live = builder.public\n .route({\n method: 'GET',\n // A path of its own, not a child of the table: `/{table}/live`\n // would make the id \"live\" unreachable on the get route, and a\n // colon suffix is a wildcard parameter in the oRPC matcher.\n path: `/api/live/${name}`,\n summary: `Live view of ${name}`,\n tags: [name],\n // Filters arrive as one URL-encoded JSON value, so no per-key query\n // parsing rule is needed.\n queryStyles: { filters: 'json' },\n })\n .input(liveQuerySchema)\n .output(eventIterator(liveFrameSchema))\n .handler(({ input, context, signal }) => {\n if (!livePublisher) {\n throw new Error(`Realtime is not enabled for live view of ${name}`)\n }\n // Subscribe before anything awaits, so no change slips between the\n // snapshot read and the start of the stream; events that arrive\n // during the query buffer in the publisher and replay on first pull.\n const changes = filterTableChanges(\n livePublisher.subscribe('change', { signal }),\n {\n tableName: schemaKey,\n entry: access,\n rule: access.list,\n request: context.request,\n getSession: context.getSession,\n },\n )\n\n return withRealtimeHeartbeat(\n (async function* () {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n let view: ReturnType<typeof createLiveWindow> | undefined\n\n const readSnapshot = async (): Promise<LiveSnapshotFrame> => {\n const result = await operations.list(name, input ?? {}, execCtx)\n view = createLiveWindow({\n sort: result.sort,\n order: result.order,\n limit: result.limit,\n filters: input?.filters as\n | Record<string, unknown>\n | undefined,\n })\n view.reset(result.items, result.hasMore)\n return {\n type: 'snapshot',\n items: result.items,\n sort: result.sort,\n order: result.order,\n limit: result.limit,\n hasMore: result.hasMore,\n }\n }\n\n yield await readSnapshot()\n\n for await (const change of changes) {\n const outcome = view!.apply(change)\n if (outcome.type === 'none') continue\n if (outcome.type === 'resnapshot') {\n yield await readSnapshot()\n continue\n }\n for (const frame of outcome.frames) yield frame\n }\n })(),\n { intervalMs: REALTIME_HEARTBEAT_INTERVAL_MS, signal },\n )\n })\n\n const procedures = {\n list,\n get,\n create,\n update,\n delete: deleteProc,\n live,\n }\n\n if (!livePublisher) {\n delete (procedures as { live?: unknown }).live\n }\n\n return procedures\n}\n\nexport type TableCrudProcedures<\n TTable extends Table,\n TFilterable extends string = string,\n TSortable extends string = string,\n> = ReturnType<\n typeof buildTableCrudProcedures<\n Record<string, unknown>,\n TTable,\n TFilterable,\n TSortable\n >\n>\n\nexport function buildCrudApiRouter<\n TSchema extends Record<string, unknown>,\n TAccess extends Record<string, TableAccessInput> | undefined = undefined,\n>(\n schema: TSchema,\n db: AnyDb,\n options: CrudApiRouterOptions<TSchema>,\n): CrudApiRouterFor<TSchema, TAccess> {\n const { access, realtime, idempotency, livePublisher } = options\n const builder = createApiBuilder<TSchema>()\n const operations = createCrudOperations({\n schema,\n db,\n access,\n idempotency,\n realtime,\n })\n\n const routerObj: Record<string, unknown> = {}\n\n for (const [tableKey, table] of Object.entries(schema)) {\n if (!isTable(table)) continue\n\n const name = getTableName(table)\n const tableAccess = tableEntryForName(access, name)\n if (!tableAccess?.enabled) continue\n\n const idCol = getTableColumns(table)['id']\n if (!idCol) continue\n\n const procedures = buildTableCrudProcedures({\n table: table as Table,\n operations,\n builder,\n access: tableAccess,\n schemaKey: tableKey,\n livePublisher,\n })\n\n routerObj[tableKey] = procedures\n }\n\n return routerObj as CrudApiRouterFor<TSchema, TAccess>\n}\n"]}
1
+ {"version":3,"file":"crud-router.js","sourceRoot":"","sources":["../../src/api/crud-router.ts"],"names":[],"mappings":"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","sourcesContent":["import { eventIterator } from '@orpc/server'\nimport { getTableColumns, getTableName, isTable, type Table } from 'drizzle-orm'\nimport '@orpc/openapi/extensions/route'\nimport {\n createInsertSchema,\n createSelectSchema,\n createUpdateSchema,\n} from 'drizzle-valibot'\nimport * as v from 'valibot'\n\nimport type { AnyDb } from '../dialect'\nimport type { IdempotencyConfig } from '../idempotency'\nimport type { LiveSnapshotFrame } from '../live/protocol'\nimport type { RealtimeFacade } from '../realtime/facade'\nimport type { RealtimePublisher } from '../realtime/publisher'\nimport type { CrudApiRouterFor } from './types'\n\nimport {\n tableEntryForName,\n type ResolvedAccess,\n type ResolvedTableAccess,\n type TableAccessInput,\n} from '../access'\nimport { createCrudOperations, type CrudOperations } from '../crud-operations'\nimport { BunderstackError } from '../errors'\nimport { filterTableChanges } from '../realtime/filter'\nimport {\n REALTIME_HEARTBEAT_INTERVAL_MS,\n withRealtimeHeartbeat,\n} from '../realtime/heartbeat'\nimport { createApiBuilder } from './builder'\nimport { buildListInputSchema, buildLiveInputSchema } from './list-input-schema'\nimport { createLiveWindow } from './live-window'\n\nexport type CrudApiRouterOptions<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n> = {\n access: ResolvedAccess\n idempotency?: boolean | IdempotencyConfig\n realtime?: RealtimeFacade<TSchema>\n /**\n * The raw realtime publisher, present when realtime is enabled. The CRUD\n * router subscribes on behalf of live views (`GET /{table}:live`);\n * publishing stays behind the facade.\n */\n livePublisher?: RealtimePublisher\n}\n\nfunction strictObject<TEntries extends v.ObjectEntries>(schema: {\n entries: TEntries\n}) {\n return v.strictObject(schema.entries)\n}\n\ntype CrudInsert<TTable extends Table> = Partial<TTable['$inferInsert']>\ntype CrudUpdate<TTable extends Table> = Partial<\n Omit<TTable['$inferInsert'], 'id'>\n>\n\n/** A filter accepts one value (`=`), a list (`IN`), or null (`IS NULL`). */\nexport type ListFilterValue<T> = T | readonly T[] | null | 'null'\n\nexport type ListFilters<TTable extends Table, TFilterable extends string> = {\n [K in Extract<keyof TTable['$inferSelect'], TFilterable>]?: ListFilterValue<\n TTable['$inferSelect'][K]\n >\n}\n\n/**\n * The `list` input as callers see it. Filter columns and sortable columns come\n * from the table's `access` entry, so `filters` autocompletes to real columns\n * and rejects wrong value types at compile time.\n */\nexport type ListInputFor<\n TTable extends Table,\n TFilterable extends string,\n TSortable extends string,\n> =\n | {\n limit?: number\n offset?: number\n cursor?: string\n sort?: TSortable\n order?: 'asc' | 'desc'\n q?: string\n count?: boolean\n filters?: ListFilters<TTable, TFilterable>\n }\n | undefined\n\n/**\n * The live-view input as callers see it: the list contract narrowed to what a\n * stream can honor — no text search, no pagination.\n */\nexport type LiveInputFor<\n TTable extends Table,\n TFilterable extends string,\n TSortable extends string,\n> =\n | {\n limit?: number\n sort?: TSortable\n order?: 'asc' | 'desc'\n filters?: ListFilters<TTable, TFilterable>\n }\n | undefined\n\nexport type BuildTableCrudProceduresArgs<\n TSchema extends Record<string, unknown>,\n TTable extends Table,\n> = {\n table: TTable\n operations: CrudOperations\n builder: ReturnType<typeof createApiBuilder<TSchema>>\n access: ResolvedTableAccess\n /** The schema key events are published under. */\n schemaKey: string\n /** Present when realtime is enabled; without it no live procedure is built. */\n livePublisher?: RealtimePublisher\n}\n\nexport function buildTableCrudProcedures<\n TSchema extends Record<string, unknown>,\n TTable extends Table,\n TFilterable extends string = string,\n TSortable extends string = string,\n>(args: BuildTableCrudProceduresArgs<TSchema, TTable>) {\n const { table, operations, builder, access, schemaKey, livePublisher } = args\n const name = getTableName(table)\n\n // drizzle-valibot infers its schemas through internal generics. Left as-is,\n // declaration emit inlines those generics by name into the published `.d.ts`,\n // where they are unbound — every column silently becomes optional for the\n // consumer. Restating the type keeps the runtime schema and makes the emitted\n // one both correct and readable.\n const selectSchema = strictObject(\n createSelectSchema(table),\n ) as unknown as v.GenericSchema<\n TTable['$inferSelect'],\n TTable['$inferSelect']\n >\n const generatedInsertSchema = strictObject(createInsertSchema(table))\n const generatedColumns = Object.keys(generatedInsertSchema.entries)\n const columns = getTableColumns(table)\n const serverManagedColumns = [\n ...access.readonlyColumns,\n ...(access.writeScope && generatedColumns.includes('organizationId')\n ? ['organizationId']\n : []),\n ...Object.entries(columns)\n .filter(([, column]) => !column.notNull || column.hasDefault)\n .map(([column]) => column),\n ].filter((column) => generatedColumns.includes(column))\n const insertEntries: Record<\n string,\n v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>\n > = {\n ...generatedInsertSchema.entries,\n }\n for (const column of serverManagedColumns) {\n const schema = insertEntries[column]\n if (schema) insertEntries[column] = v.optional(schema)\n }\n const insertSchema = v.strictObject(\n insertEntries,\n ) as unknown as v.GenericSchema<CrudInsert<TTable>, CrudInsert<TTable>>\n const generatedUpdateSchema = createUpdateSchema(table)\n const updateBodySchema = v.omit(generatedUpdateSchema, [\n 'id' as keyof typeof generatedUpdateSchema.entries,\n ])\n // Compact input is transport-neutral: direct oRPC callers pass\n // `{ id, ...changes }`, while OpenAPI still maps `id` to the path and the\n // remaining fields to the PATCH body.\n const updateInputSchema = v.strictObject({\n id: v.string(),\n ...updateBodySchema.entries,\n }) as unknown as v.GenericSchema<\n { id: string } & CrudUpdate<TTable>,\n { id: string } & CrudUpdate<TTable>\n >\n\n // One filter field per allowed column, typed by the column itself: a scalar\n // for `=`, a list for `IN`, and `null` for `IS NULL`. Query strings are\n // coerced to these types by SmartCoercionHandlerPlugin, so REST and RPC share\n // one contract and nothing has to re-read the raw URL.\n // The cast restates the schema with the literals the caller's `access`\n // config carries, which the runtime column lists cannot name on their own.\n const listQuerySchema = buildListInputSchema(table, {\n filterableColumns: access.filterableColumns,\n sortableColumns: access.sortableColumns,\n }) as unknown as v.GenericSchema<\n ListInputFor<TTable, TFilterable, TSortable>,\n ListInputFor<TTable, TFilterable, TSortable>\n >\n\n const listOutputSchema = v.strictObject({\n items: v.array(selectSchema),\n nextCursor: v.optional(v.string()),\n hasMore: v.boolean(),\n total: v.optional(v.number()),\n limit: v.optional(v.number()),\n offset: v.optional(v.number()),\n cursor: v.optional(v.string()),\n q: v.optional(v.string()),\n sort: v.optional(v.string()),\n order: v.optional(v.string()),\n })\n\n // 1. LIST procedure\n const list = builder.public\n .route({\n method: 'GET',\n path: `/api/${name}`,\n summary: `List ${name}`,\n tags: [name],\n })\n .input(listQuerySchema)\n .output(listOutputSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n const result = await operations.list(name, input ?? {}, execCtx)\n return {\n ...result,\n items: result.items as TTable['$inferSelect'][],\n }\n })\n\n // 2. GET procedure\n const get = builder.public\n .route({\n method: 'GET',\n path: `/api/${name}/{id}`,\n summary: `Get ${name} by ID`,\n tags: [name],\n })\n .input(v.strictObject({ id: v.string() }))\n .output(selectSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n return (await operations.get(\n name,\n input.id,\n execCtx,\n )) as TTable['$inferSelect']\n })\n\n // 3. CREATE procedure\n const create = builder.public\n .route({\n method: 'POST',\n path: `/api/${name}`,\n summary: `Create ${name}`,\n tags: [name],\n successStatus: 201,\n })\n .input(insertSchema)\n .output(selectSchema)\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n const idempotencyKey = context.request.headers\n .get('Idempotency-Key')\n ?.trim()\n const rawBody = await context.getRawBody()\n\n const res = await operations.create(\n name,\n input,\n rawBody,\n idempotencyKey,\n execCtx,\n )\n if (res.type === 'replay') {\n context.resHeaders.set('Idempotency-Replayed', 'true')\n return res.record as TTable['$inferSelect']\n }\n return res.record as TTable['$inferSelect']\n })\n\n // 4. UPDATE procedure\n const update = builder.public\n .route({\n method: 'PATCH',\n path: `/api/${name}/{id}`,\n summary: `Update ${name}`,\n tags: [name],\n })\n .input(updateInputSchema)\n .output(selectSchema)\n .handler(async ({ input, context }) => {\n const requestPath = new URL(context.request.url).pathname\n if (!requestPath.startsWith('/api/rpc')) {\n const rawBody = await context.getRawBody()\n if (rawBody) {\n const body = JSON.parse(rawBody) as Record<string, unknown>\n if (Object.hasOwn(body, 'id')) {\n throw new BunderstackError(\n 'BAD_REQUEST',\n 'The id field is immutable',\n )\n }\n }\n }\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n\n const { id, ...changes } = input\n return (await operations.update(\n name,\n id,\n changes,\n execCtx,\n )) as TTable['$inferSelect']\n })\n\n // 5. DELETE procedure\n const deleteProc = builder.public\n .route({\n method: 'DELETE',\n path: `/api/${name}/{id}`,\n summary: `Delete ${name}`,\n tags: [name],\n successStatus: 204,\n })\n .input(v.strictObject({ id: v.string() }))\n .output(v.undefined())\n .handler(async ({ input, context }) => {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n await operations.delete(name, input.id, execCtx)\n return undefined\n })\n\n // 6. LIVE procedure — one list query as a stream. A snapshot first, then the\n // changes this view cares about, placed by the server. Every connection\n // opens with a snapshot, so a reconnect is its own recovery: no client-side\n // event buffer, no Last-Event-ID bookkeeping, and no refetch path.\n const liveQuerySchema = buildLiveInputSchema(table, {\n filterableColumns: access.filterableColumns,\n sortableColumns: access.sortableColumns,\n }) as unknown as v.GenericSchema<\n LiveInputFor<TTable, TFilterable, TSortable>,\n LiveInputFor<TTable, TFilterable, TSortable>\n >\n\n const liveFrameSchema = v.union([\n v.strictObject({\n type: v.literal('snapshot'),\n items: v.array(selectSchema),\n sort: v.string(),\n order: v.picklist(['asc', 'desc']),\n limit: v.number(),\n hasMore: v.boolean(),\n operationId: v.optional(v.string()),\n }),\n v.strictObject({\n type: v.literal('upsert'),\n record: selectSchema,\n afterId: v.union([v.string(), v.null()]),\n operationId: v.optional(v.string()),\n }),\n v.strictObject({\n type: v.literal('remove'),\n id: v.string(),\n operationId: v.optional(v.string()),\n }),\n v.strictObject({ type: v.literal('heartbeat'), intervalMs: v.number() }),\n ])\n\n const live = builder.public\n .route({\n method: 'GET',\n // A path of its own, not a child of the table: `/{table}/live`\n // would make the id \"live\" unreachable on the get route, and a\n // colon suffix is a wildcard parameter in the oRPC matcher.\n path: `/api/live/${name}`,\n summary: `Live view of ${name}`,\n tags: [name],\n // Filters arrive as one URL-encoded JSON value, so no per-key query\n // parsing rule is needed.\n queryStyles: { filters: 'json' },\n })\n .input(liveQuerySchema)\n .output(eventIterator(liveFrameSchema))\n .handler(({ input, context, signal }) => {\n if (!livePublisher) {\n throw new Error(`Realtime is not enabled for live view of ${name}`)\n }\n // Subscribe before anything awaits, so no change slips between the\n // snapshot read and the start of the stream; events that arrive\n // during the query buffer in the publisher and replay on first pull.\n const changes = filterTableChanges(\n livePublisher.subscribe('change', { signal }),\n {\n tableName: schemaKey,\n entry: access,\n rule: access.list,\n request: context.request,\n getSession: context.getSession,\n },\n )\n\n return withRealtimeHeartbeat(\n (async function* () {\n const session = await context.getSession()\n const execCtx = {\n request: context.request,\n user: session.user,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n let view: ReturnType<typeof createLiveWindow> | undefined\n\n const readSnapshot = async (\n operationId?: string,\n ): Promise<LiveSnapshotFrame<TTable['$inferSelect']>> => {\n const result = await operations.list(name, input ?? {}, execCtx)\n view = createLiveWindow({\n sort: result.sort,\n order: result.order,\n limit: result.limit,\n filters: input?.filters as Record<string, unknown> | undefined,\n })\n view.reset(result.items, result.hasMore)\n return {\n type: 'snapshot',\n items: result.items,\n sort: result.sort,\n order: result.order,\n limit: result.limit,\n hasMore: result.hasMore,\n ...(operationId ? { operationId } : {}),\n }\n }\n\n yield await readSnapshot()\n\n for await (const change of changes) {\n const outcome = view!.apply(change)\n if (outcome.type === 'none') {\n // The write succeeded but did not affect this view. A snapshot\n // still acknowledges the operation without inventing a row.\n if (change.operationId)\n yield await readSnapshot(change.operationId)\n continue\n }\n if (outcome.type === 'resnapshot') {\n yield await readSnapshot(change.operationId)\n continue\n }\n for (const frame of outcome.frames) yield frame\n }\n })(),\n { intervalMs: REALTIME_HEARTBEAT_INTERVAL_MS, signal },\n )\n })\n\n const procedures = {\n list,\n get,\n create,\n update,\n delete: deleteProc,\n live,\n }\n\n if (!livePublisher) {\n delete (procedures as { live?: unknown }).live\n }\n\n return procedures\n}\n\nexport type TableCrudProcedures<\n TTable extends Table,\n TFilterable extends string = string,\n TSortable extends string = string,\n TLive extends boolean = false,\n> = Omit<\n ReturnType<\n typeof buildTableCrudProcedures<\n Record<string, unknown>,\n TTable,\n TFilterable,\n TSortable\n >\n >,\n 'live'\n> &\n (TLive extends true\n ? {\n live: ReturnType<\n typeof buildTableCrudProcedures<\n Record<string, unknown>,\n TTable,\n TFilterable,\n TSortable\n >\n >['live']\n }\n : {})\n\nexport function buildCrudApiRouter<\n TSchema extends Record<string, unknown>,\n TAccess extends Record<string, TableAccessInput> | undefined = undefined,\n TLive extends boolean = false,\n>(\n schema: TSchema,\n db: AnyDb,\n options: CrudApiRouterOptions<TSchema>,\n): CrudApiRouterFor<TSchema, TAccess, TLive> {\n const { access, realtime, idempotency, livePublisher } = options\n const builder = createApiBuilder<TSchema>()\n const operations = createCrudOperations({\n schema,\n db,\n access,\n idempotency,\n realtime,\n })\n\n const routerObj: Record<string, unknown> = {}\n\n for (const [tableKey, table] of Object.entries(schema)) {\n if (!isTable(table)) continue\n\n const name = getTableName(table)\n const tableAccess = tableEntryForName(access, name)\n if (!tableAccess?.enabled) continue\n\n const idCol = getTableColumns(table)['id']\n if (!idCol) continue\n\n const procedures = buildTableCrudProcedures({\n table: table as Table,\n operations,\n builder,\n access: tableAccess,\n schemaKey: tableKey,\n livePublisher,\n })\n\n routerObj[tableKey] = procedures\n }\n\n return routerObj as CrudApiRouterFor<TSchema, TAccess, TLive>\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"live-window.d.ts","sourceRoot":"","sources":["../../src/api/live-window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAI3D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,KAAK,GAAG,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAA;AAE1B,MAAM,MAAM,UAAU,GAAG;IACvB,gDAAgD;IAChD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IACnE,4CAA4C;IAC5C,KAAK,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,gBAAgB,CAAA;CACpD,CAAA;AAmBD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAsEvE"}
1
+ {"version":3,"file":"live-window.d.ts","sourceRoot":"","sources":["../../src/api/live-window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAI3D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,KAAK,GAAG,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAA;AAE1B,MAAM,MAAM,UAAU,GAAG;IACvB,gDAAgD;IAChD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IACnE,4CAA4C;IAC5C,KAAK,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,gBAAgB,CAAA;CACpD,CAAA;AAmBD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAgFvE"}
@@ -38,6 +38,9 @@ export function createLiveWindow(options) {
38
38
  },
39
39
  apply(change) {
40
40
  const id = String(change.record.id);
41
+ const metadata = change.operationId
42
+ ? { operationId: change.operationId }
43
+ : {};
41
44
  const index = rows.findIndex((row) => row.id === id);
42
45
  const held = index !== -1;
43
46
  // The row left the view: deleted, or no longer a match for the filters.
@@ -50,7 +53,10 @@ export function createLiveWindow(options) {
50
53
  // database knows which one.
51
54
  if (hasMore)
52
55
  return { type: 'resnapshot' };
53
- return { type: 'frames', frames: [{ type: 'remove', id }] };
56
+ return {
57
+ type: 'frames',
58
+ frames: [{ type: 'remove', id, ...metadata }],
59
+ };
54
60
  }
55
61
  if (held)
56
62
  rows.splice(index, 1);
@@ -61,7 +67,10 @@ export function createLiveWindow(options) {
61
67
  if (!held)
62
68
  return { type: 'none' };
63
69
  hasMore = true;
64
- return { type: 'frames', frames: [{ type: 'remove', id }] };
70
+ return {
71
+ type: 'frames',
72
+ frames: [{ type: 'remove', id, ...metadata }],
73
+ };
65
74
  }
66
75
  rows.splice(place, 0, row);
67
76
  const frames = [
@@ -69,12 +78,13 @@ export function createLiveWindow(options) {
69
78
  type: 'upsert',
70
79
  record: change.record,
71
80
  afterId: place === 0 ? null : rows[place - 1].id,
81
+ ...metadata,
72
82
  },
73
83
  ];
74
84
  if (rows.length > options.limit) {
75
85
  const evicted = rows.pop();
76
86
  hasMore = true;
77
- frames.push({ type: 'remove', id: evicted.id });
87
+ frames.push({ type: 'remove', id: evicted.id, ...metadata });
78
88
  }
79
89
  return { type: 'frames', frames };
80
90
  },
@@ -1 +1 @@
1
- {"version":3,"file":"live-window.js","sourceRoot":"","sources":["../../src/api/live-window.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LiveDeltaFrame } from '../live/protocol'\nimport type { RealtimeChange } from '../realtime/publisher'\n\nimport { matchesLiveFilters } from './live-view'\n\n/**\n * The server-side state of one live-view connection: which rows the client\n * holds, in view order, and whether rows exist below the window.\n *\n * The window is what lets the server place a row for the client (`afterId`)\n * instead of making the browser repeat `ORDER BY`. It is also what makes a\n * removal from a truncated view ask for a fresh snapshot instead of silently\n * shrinking the view.\n *\n * Ordering follows `buildOrderBy` in `../list-query`: the sort column first,\n * then `id`, both in the query's direction. Values are compared in the process\n * on raw column values. That is exact for numbers, dates, and booleans, and\n * matches the database for ordinary text. A collation that differs from\n * code-unit order, or a NULLS rule other than \"nulls first\", can place a row\n * inside the window differently from the database.\n */\n\nexport type LiveWindowOptions = {\n sort: string\n order: 'asc' | 'desc'\n limit: number\n filters?: Record<string, unknown>\n}\n\nexport type LiveWindowResult =\n | { type: 'none' }\n | { type: 'frames'; frames: LiveDeltaFrame[] }\n | { type: 'resnapshot' }\n\nexport type LiveWindow = {\n /** Adopt a query result as the current view. */\n reset: (items: Record<string, unknown>[], hasMore: boolean) => void\n /** What this change means for this view. */\n apply: (change: RealtimeChange) => LiveWindowResult\n}\n\ntype WindowRow = { id: string; key: unknown }\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0\n const aNull = a === null || a === undefined\n const bNull = b === null || b === undefined\n if (aNull || bNull) return aNull ? (bNull ? 0 : -1) : 1\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return Number(a) - Number(b)\n }\n const left = String(a)\n const right = String(b)\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nexport function createLiveWindow(options: LiveWindowOptions): LiveWindow {\n const direction = options.order === 'desc' ? -1 : 1\n let rows: WindowRow[] = []\n let hasMore = false\n\n const compareRows = (a: WindowRow, b: WindowRow): number =>\n direction * (compareValues(a.key, b.key) || compareValues(a.id, b.id))\n\n const rowOf = (record: Record<string, unknown>): WindowRow => ({\n id: String(record.id),\n key: record[options.sort],\n })\n\n /** Where this row belongs among rows that are already in order. */\n const placeOf = (row: WindowRow): number => {\n const index = rows.findIndex((current) => compareRows(row, current) < 0)\n return index === -1 ? rows.length : index\n }\n\n return {\n reset(items, nextHasMore) {\n rows = items.map(rowOf)\n hasMore = nextHasMore\n },\n\n apply(change) {\n const id = String(change.record.id)\n const index = rows.findIndex((row) => row.id === id)\n const held = index !== -1\n\n // The row left the view: deleted, or no longer a match for the filters.\n if (\n change.action === 'delete' ||\n !matchesLiveFilters(change.record, options.filters)\n ) {\n if (!held) return { type: 'none' }\n rows.splice(index, 1)\n // A row from below the window must take the free place, and only the\n // database knows which one.\n if (hasMore) return { type: 'resnapshot' }\n return { type: 'frames', frames: [{ type: 'remove', id }] }\n }\n\n if (held) rows.splice(index, 1)\n const row = rowOf(change.record)\n const place = placeOf(row)\n\n if (place >= options.limit) {\n // It sorts below the window. If the client holds it, it moved out.\n if (!held) return { type: 'none' }\n hasMore = true\n return { type: 'frames', frames: [{ type: 'remove', id }] }\n }\n\n rows.splice(place, 0, row)\n const frames: LiveDeltaFrame[] = [\n {\n type: 'upsert',\n record: change.record,\n afterId: place === 0 ? null : rows[place - 1]!.id,\n },\n ]\n if (rows.length > options.limit) {\n const evicted = rows.pop()!\n hasMore = true\n frames.push({ type: 'remove', id: evicted.id })\n }\n return { type: 'frames', frames }\n },\n }\n}\n"]}
1
+ {"version":3,"file":"live-window.js","sourceRoot":"","sources":["../../src/api/live-window.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LiveDeltaFrame } from '../live/protocol'\nimport type { RealtimeChange } from '../realtime/publisher'\n\nimport { matchesLiveFilters } from './live-view'\n\n/**\n * The server-side state of one live-view connection: which rows the client\n * holds, in view order, and whether rows exist below the window.\n *\n * The window is what lets the server place a row for the client (`afterId`)\n * instead of making the browser repeat `ORDER BY`. It is also what makes a\n * removal from a truncated view ask for a fresh snapshot instead of silently\n * shrinking the view.\n *\n * Ordering follows `buildOrderBy` in `../list-query`: the sort column first,\n * then `id`, both in the query's direction. Values are compared in the process\n * on raw column values. That is exact for numbers, dates, and booleans, and\n * matches the database for ordinary text. A collation that differs from\n * code-unit order, or a NULLS rule other than \"nulls first\", can place a row\n * inside the window differently from the database.\n */\n\nexport type LiveWindowOptions = {\n sort: string\n order: 'asc' | 'desc'\n limit: number\n filters?: Record<string, unknown>\n}\n\nexport type LiveWindowResult =\n | { type: 'none' }\n | { type: 'frames'; frames: LiveDeltaFrame[] }\n | { type: 'resnapshot' }\n\nexport type LiveWindow = {\n /** Adopt a query result as the current view. */\n reset: (items: Record<string, unknown>[], hasMore: boolean) => void\n /** What this change means for this view. */\n apply: (change: RealtimeChange) => LiveWindowResult\n}\n\ntype WindowRow = { id: string; key: unknown }\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0\n const aNull = a === null || a === undefined\n const bNull = b === null || b === undefined\n if (aNull || bNull) return aNull ? (bNull ? 0 : -1) : 1\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return Number(a) - Number(b)\n }\n const left = String(a)\n const right = String(b)\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nexport function createLiveWindow(options: LiveWindowOptions): LiveWindow {\n const direction = options.order === 'desc' ? -1 : 1\n let rows: WindowRow[] = []\n let hasMore = false\n\n const compareRows = (a: WindowRow, b: WindowRow): number =>\n direction * (compareValues(a.key, b.key) || compareValues(a.id, b.id))\n\n const rowOf = (record: Record<string, unknown>): WindowRow => ({\n id: String(record.id),\n key: record[options.sort],\n })\n\n /** Where this row belongs among rows that are already in order. */\n const placeOf = (row: WindowRow): number => {\n const index = rows.findIndex((current) => compareRows(row, current) < 0)\n return index === -1 ? rows.length : index\n }\n\n return {\n reset(items, nextHasMore) {\n rows = items.map(rowOf)\n hasMore = nextHasMore\n },\n\n apply(change) {\n const id = String(change.record.id)\n const metadata = change.operationId\n ? { operationId: change.operationId }\n : {}\n const index = rows.findIndex((row) => row.id === id)\n const held = index !== -1\n\n // The row left the view: deleted, or no longer a match for the filters.\n if (\n change.action === 'delete' ||\n !matchesLiveFilters(change.record, options.filters)\n ) {\n if (!held) return { type: 'none' }\n rows.splice(index, 1)\n // A row from below the window must take the free place, and only the\n // database knows which one.\n if (hasMore) return { type: 'resnapshot' }\n return {\n type: 'frames',\n frames: [{ type: 'remove', id, ...metadata }],\n }\n }\n\n if (held) rows.splice(index, 1)\n const row = rowOf(change.record)\n const place = placeOf(row)\n\n if (place >= options.limit) {\n // It sorts below the window. If the client holds it, it moved out.\n if (!held) return { type: 'none' }\n hasMore = true\n return {\n type: 'frames',\n frames: [{ type: 'remove', id, ...metadata }],\n }\n }\n\n rows.splice(place, 0, row)\n const frames: LiveDeltaFrame[] = [\n {\n type: 'upsert',\n record: change.record,\n afterId: place === 0 ? null : rows[place - 1]!.id,\n ...metadata,\n },\n ]\n if (rows.length > options.limit) {\n const evicted = rows.pop()!\n hasMore = true\n frames.push({ type: 'remove', id: evicted.id, ...metadata })\n }\n return { type: 'frames', frames }\n },\n }\n}\n"]}
@@ -15,6 +15,7 @@ export declare function buildRealtimeApiRouter(publisher: RealtimePublisher | un
15
15
  record: {
16
16
  [x: string]: unknown;
17
17
  };
18
+ operationId?: string | undefined;
18
19
  } | {
19
20
  type: "heartbeat";
20
21
  intervalMs: number;
@@ -24,6 +25,7 @@ export declare function buildRealtimeApiRouter(publisher: RealtimePublisher | un
24
25
  record: {
25
26
  [x: string]: unknown;
26
27
  };
28
+ operationId?: string | undefined;
27
29
  } | {
28
30
  type: "heartbeat";
29
31
  intervalMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"realtime-router.d.ts","sourceRoot":"","sources":["../../src/api/realtime-router.ts"],"names":[],"mappings":"AACA,OAAO,gCAAgC,CAAA;AACvC,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAwB9D,KAAK,qBAAqB,GAAG;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,iBAAiB,GAAG,SAAS,EACxC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,qBAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqCpC;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,CACzC,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAC1C,CAAA"}
1
+ {"version":3,"file":"realtime-router.d.ts","sourceRoot":"","sources":["../../src/api/realtime-router.ts"],"names":[],"mappings":"AACA,OAAO,gCAAgC,CAAA;AACvC,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAyB9D,KAAK,qBAAqB,GAAG;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,iBAAiB,GAAG,SAAS,EACxC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,qBAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqCpC;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,CACzC,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAC1C,CAAA"}
@@ -9,6 +9,7 @@ const changeSchema = v.strictObject({
9
9
  table: v.string(),
10
10
  action: v.picklist(['create', 'update', 'delete']),
11
11
  record: v.record(v.string(), v.unknown()),
12
+ operationId: v.optional(v.string()),
12
13
  });
13
14
  const heartbeatSchema = v.strictObject({
14
15
  type: v.literal('heartbeat'),
@@ -1 +1 @@
1
- {"version":3,"file":"realtime-router.js","sourceRoot":"","sources":["../../src/api/realtime-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,gCAAgC,CAAA;AACvC,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAK5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CACzB,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAC1C,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CACjE,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;IAClC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;CAC1C,CAAC,CAAA;AAEF,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;IAC5B,wEAAwE;IACxE,sDAAsD;IACtD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;CACvB,CAAC,CAAA;AAMF,MAAM,UAAU,sBAAsB,CACpC,SAAwC,EACxC,MAAsB,EACtB,UAAiC,EAAE;IAEnC,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAA;IAChC,MAAM,OAAO,GAAG,gBAAgB,EAG7B,CAAA;IAEH,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM;SAC3B,KAAK,CAAC;QACL,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,+BAA+B;QACxC,IAAI,EAAE,CAAC,UAAU,CAAC;QAClB,WAAW,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;KACjC,CAAC;SACD,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;SAC/C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;SAC/D,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,CACnD,qBAAqB,CACnB,qBAAqB,CACnB,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,EACtD;QACE,aAAa,EAAE,KAAK,CAAC,MAAM;QAC3B,MAAM;QACN,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CACF,EACD;QACE,UAAU,EAAE,OAAO,CAAC,WAAW;QAC/B,MAAM;KACP,CACF,CACF,CAAA;IAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,EAAE,CAAA;AAClC,CAAC","sourcesContent":["import { eventIterator } from '@orpc/server'\nimport '@orpc/openapi/extensions/route'\nimport * as v from 'valibot'\n\nimport type { ResolvedAccess } from '../access'\nimport type { RealtimePublisher } from '../realtime/publisher'\n\nimport { filterRealtimeChanges } from '../realtime/filter'\nimport { withRealtimeHeartbeat } from '../realtime/heartbeat'\nimport { createApiBuilder } from './builder'\n\nconst tablesSchema = v.pipe(\n v.union([v.string(), v.array(v.string())]),\n v.transform((value) => (Array.isArray(value) ? value : [value])),\n)\n\nconst changeSchema = v.strictObject({\n table: v.string(),\n action: v.picklist(['create', 'update', 'delete']),\n record: v.record(v.string(), v.unknown()),\n})\n\nconst heartbeatSchema = v.strictObject({\n type: v.literal('heartbeat'),\n // The cadence this stream sends heartbeats at, so a client can size its\n // dead-stream timeout from the server's real setting.\n intervalMs: v.number(),\n})\n\ntype RealtimeRouterOptions = {\n heartbeatMs?: number\n}\n\nexport function buildRealtimeApiRouter(\n publisher: RealtimePublisher | undefined,\n access: ResolvedAccess,\n options: RealtimeRouterOptions = {},\n) {\n if (!publisher) return undefined\n const builder = createApiBuilder<\n Record<string, unknown>,\n Record<string, unknown>\n >()\n\n const changes = builder.public\n .route({\n method: 'GET',\n path: '/api/realtime',\n summary: 'Subscribe to realtime changes',\n tags: ['realtime'],\n queryStyles: { tables: 'array' },\n })\n .input(v.strictObject({ tables: tablesSchema }))\n .output(eventIterator(v.union([changeSchema, heartbeatSchema])))\n .handler(({ input, context, signal, lastEventId }) =>\n withRealtimeHeartbeat(\n filterRealtimeChanges(\n publisher.subscribe('change', { signal, lastEventId }),\n {\n subscriptions: input.tables,\n access,\n request: context.request,\n getSession: context.getSession,\n },\n ),\n {\n intervalMs: options.heartbeatMs,\n signal,\n },\n ),\n )\n\n return { realtime: { changes } }\n}\n\nexport type RealtimeApiRouter = NonNullable<\n ReturnType<typeof buildRealtimeApiRouter>\n>\n"]}
1
+ {"version":3,"file":"realtime-router.js","sourceRoot":"","sources":["../../src/api/realtime-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,gCAAgC,CAAA;AACvC,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAK5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CACzB,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAC1C,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CACjE,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;IAClC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IACzC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;CACpC,CAAC,CAAA;AAEF,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;IAC5B,wEAAwE;IACxE,sDAAsD;IACtD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;CACvB,CAAC,CAAA;AAMF,MAAM,UAAU,sBAAsB,CACpC,SAAwC,EACxC,MAAsB,EACtB,UAAiC,EAAE;IAEnC,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAA;IAChC,MAAM,OAAO,GAAG,gBAAgB,EAG7B,CAAA;IAEH,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM;SAC3B,KAAK,CAAC;QACL,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,+BAA+B;QACxC,IAAI,EAAE,CAAC,UAAU,CAAC;QAClB,WAAW,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;KACjC,CAAC;SACD,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;SAC/C,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;SAC/D,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,CACnD,qBAAqB,CACnB,qBAAqB,CACnB,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,EACtD;QACE,aAAa,EAAE,KAAK,CAAC,MAAM;QAC3B,MAAM;QACN,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CACF,EACD;QACE,UAAU,EAAE,OAAO,CAAC,WAAW;QAC/B,MAAM;KACP,CACF,CACF,CAAA;IAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,EAAE,CAAA;AAClC,CAAC","sourcesContent":["import { eventIterator } from '@orpc/server'\nimport '@orpc/openapi/extensions/route'\nimport * as v from 'valibot'\n\nimport type { ResolvedAccess } from '../access'\nimport type { RealtimePublisher } from '../realtime/publisher'\n\nimport { filterRealtimeChanges } from '../realtime/filter'\nimport { withRealtimeHeartbeat } from '../realtime/heartbeat'\nimport { createApiBuilder } from './builder'\n\nconst tablesSchema = v.pipe(\n v.union([v.string(), v.array(v.string())]),\n v.transform((value) => (Array.isArray(value) ? value : [value])),\n)\n\nconst changeSchema = v.strictObject({\n table: v.string(),\n action: v.picklist(['create', 'update', 'delete']),\n record: v.record(v.string(), v.unknown()),\n operationId: v.optional(v.string()),\n})\n\nconst heartbeatSchema = v.strictObject({\n type: v.literal('heartbeat'),\n // The cadence this stream sends heartbeats at, so a client can size its\n // dead-stream timeout from the server's real setting.\n intervalMs: v.number(),\n})\n\ntype RealtimeRouterOptions = {\n heartbeatMs?: number\n}\n\nexport function buildRealtimeApiRouter(\n publisher: RealtimePublisher | undefined,\n access: ResolvedAccess,\n options: RealtimeRouterOptions = {},\n) {\n if (!publisher) return undefined\n const builder = createApiBuilder<\n Record<string, unknown>,\n Record<string, unknown>\n >()\n\n const changes = builder.public\n .route({\n method: 'GET',\n path: '/api/realtime',\n summary: 'Subscribe to realtime changes',\n tags: ['realtime'],\n queryStyles: { tables: 'array' },\n })\n .input(v.strictObject({ tables: tablesSchema }))\n .output(eventIterator(v.union([changeSchema, heartbeatSchema])))\n .handler(({ input, context, signal, lastEventId }) =>\n withRealtimeHeartbeat(\n filterRealtimeChanges(\n publisher.subscribe('change', { signal, lastEventId }),\n {\n subscriptions: input.tables,\n access,\n request: context.request,\n getSession: context.getSession,\n },\n ),\n {\n intervalMs: options.heartbeatMs,\n signal,\n },\n ),\n )\n\n return { realtime: { changes } }\n}\n\nexport type RealtimeApiRouter = NonNullable<\n ReturnType<typeof buildRealtimeApiRouter>\n>\n"]}
@@ -41,8 +41,8 @@ type FilterableOf<TAccess, K extends string> = K extends keyof TAccess ? TAccess
41
41
  type SortableOf<TAccess, K extends string> = K extends keyof TAccess ? TAccess[K] extends {
42
42
  sortableColumns: readonly (infer S extends string)[];
43
43
  } ? S : 'id' : 'id';
44
- export type CrudApiRouterFor<TSchema extends Record<string, unknown>, TAccess = undefined> = {
45
- [K in ExposedApiTables<TSchema, TAccess> as TSchema[K] extends Table ? K : never]: TableCrudProcedures<Extract<TSchema[K], Table>, FilterableOf<TAccess, K>, SortableOf<TAccess, K>>;
44
+ export type CrudApiRouterFor<TSchema extends Record<string, unknown>, TAccess = undefined, TLive extends boolean = false> = {
45
+ [K in ExposedApiTables<TSchema, TAccess> as TSchema[K] extends Table ? K : never]: TableCrudProcedures<Extract<TSchema[K], Table>, FilterableOf<TAccess, K>, SortableOf<TAccess, K>, TLive>;
46
46
  };
47
47
  type IsProcedure<T> = T extends {
48
48
  '~orpc': unknown;