@beezping/adapter-prisma 0.7.0 → 0.8.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.
package/dist/index.d.cts CHANGED
@@ -1,189 +1,7 @@
1
- import { FeedbackPayload, SitepingStore, ScreenshotStorage, FeedbackCreateInput, FeedbackRecord as FeedbackRecord$1, FeedbackQuery, FeedbackPage, FeedbackUpdateInput } from './siteping-core.cjs';
2
- export { ScreenshotStorage, SitepingStore, StoreDuplicateError, StoreNotFoundError, StorePersistenceError, flattenAnnotation, isStorePersistence } from './siteping-core.cjs';
3
- import { FeedbackStatus, FeedbackRecord, FeedbackType } from './siteping-core.js';
4
-
5
- /** HTTP methods served by `createSitepingHandler`. */
6
- type SitepingHttpMethod = "GET" | "POST" | "PATCH" | "DELETE" | "OPTIONS";
7
-
8
- /** Options of the built-in shared-secret policy (the historical `adapter-prisma` behavior). */
9
- interface ApiKeyAccessOptions {
10
- /**
11
- * Shared secret expected as `Authorization: Bearer {apiKey}`.
12
- *
13
- * - **When set:** every request not listed in `publicEndpoints` must carry it (401 otherwise).
14
- * - **When not set:** the API is public, except DELETE/PATCH while
15
- * `requireAuthForDestructive` is on.
16
- */
17
- apiKey?: string | undefined;
18
- /** Methods that skip the key. Defaults to `['POST', 'OPTIONS']` when `apiKey` is set. */
19
- publicEndpoints?: ReadonlyArray<SitepingHttpMethod>;
20
- /**
21
- * Whether DELETE/PATCH require `apiKey`. Defaults to `true`: without `apiKey`
22
- * the handler refuses to start in production and answers 401 elsewhere.
23
- * Set `false` only behind your own auth layer — or pass `access` instead.
24
- */
25
- requireAuthForDestructive?: boolean;
26
- /**
27
- * Blank `authorEmail` in responses to requests without a valid key.
28
- * Defaults to `true` — reviewer emails are PII and GET must stay reachable
29
- * by the widget.
30
- */
31
- redactUnauthenticatedEmails?: boolean;
32
- }
33
-
34
- /**
35
- * Outgoing webhook notifications for newly-created feedbacks.
36
- *
37
- * Plug a Slack, Discord, or generic HTTP endpoint into `createSitepingHandler`
38
- * to receive a payload whenever a feedback is successfully persisted. Webhooks
39
- * are dispatched as fire-and-forget (`void Promise.all(...)`) so a slow or
40
- * down receiver never blocks the client response — the feedback is already in
41
- * the DB by the time we dial out.
42
- *
43
- * - **Type-specific formatting**: Slack uses `{ text, blocks }`, Discord uses
44
- * `{ content, embeds }`, generic posts the record as JSON (minus `clientId`).
45
- * - **Untrusted input**: `message` and `authorName` come from anonymous
46
- * visitors. Slack text is escaped and Discord mention parsing is disabled,
47
- * so a public feedback form can never be turned into a channel-wide ping.
48
- * - **Timeout**: 5s by default (overridable per webhook).
49
- * - **Error handling**: `config.onError(err, feedback.id)` is invoked when
50
- * present; otherwise we log a one-liner to `console.warn` so the issue is
51
- * surfaced without crashing the request.
52
- */
53
-
54
- /** Supported webhook integrations — drives the JSON body shape. */
55
- type WebhookType = "slack" | "discord" | "generic";
56
- /**
57
- * Outgoing webhook configuration.
58
- *
59
- * - `url` — required, the HTTPS endpoint to POST to.
60
- * - `type` — payload format. Defaults to `"generic"` (raw JSON).
61
- * - `headers` — extra headers merged on top of `Content-Type: application/json`.
62
- * Useful for signed-payload schemes (`X-Signature`, bearer tokens, …).
63
- * - `timeoutMs` — abort the fetch after this many ms. Defaults to 5000.
64
- * - `onError` — invoked with the underlying error and the feedback id when
65
- * the dispatch fails (network error, non-2xx, timeout). The webhook is
66
- * fire-and-forget, so this is your only chance to observe failures.
67
- */
68
- interface WebhookConfig {
69
- url: string;
70
- type?: WebhookType;
71
- headers?: Record<string, string>;
72
- timeoutMs?: number;
73
- onError?: (err: Error, feedbackId: string) => void;
74
- }
75
- /** Block Kit envelope used by Slack incoming webhooks. */
76
- interface SlackWebhookPayload {
77
- text: string;
78
- blocks: ReadonlyArray<SlackHeaderBlock | SlackSectionBlock | SlackContextBlock>;
79
- }
80
- interface SlackHeaderBlock {
81
- type: "header";
82
- text: {
83
- type: "plain_text";
84
- text: string;
85
- emoji: true;
86
- };
87
- }
88
- interface SlackSectionBlock {
89
- type: "section";
90
- text: {
91
- type: "mrkdwn";
92
- text: string;
93
- };
94
- }
95
- interface SlackContextBlock {
96
- type: "context";
97
- elements: ReadonlyArray<{
98
- type: "mrkdwn";
99
- text: string;
100
- }>;
101
- }
102
- /** Embed envelope used by Discord incoming webhooks. */
103
- interface DiscordWebhookPayload {
104
- content: string;
105
- embeds: ReadonlyArray<{
106
- title: string;
107
- description: string;
108
- color: number;
109
- fields: ReadonlyArray<{
110
- name: string;
111
- value: string;
112
- inline: boolean;
113
- }>;
114
- timestamp: string;
115
- }>;
116
- /**
117
- * Mention parsing is switched off: `content` carries end-user text, so an
118
- * author called `@everyone` must render as text, never as a notification.
119
- */
120
- allowed_mentions: {
121
- parse: ReadonlyArray<"roles" | "users" | "everyone">;
122
- };
123
- }
124
- /**
125
- * Generic webhook body — the stored record as JSON. `clientId` is stripped like
126
- * on every other output: it is the browser-local dedup secret and the POST
127
- * replay path hands the full record to whoever presents it.
128
- */
129
- type GenericWebhookPayload = Omit<FeedbackRecord, "clientId">;
130
- /** Mapping from webhook type to its concrete body shape. */
131
- interface WebhookPayloadMap {
132
- slack: SlackWebhookPayload;
133
- discord: DiscordWebhookPayload;
134
- generic: GenericWebhookPayload;
135
- }
136
- /**
137
- * Dispatch a single webhook. Fire-and-forget: never throws, never rejects.
138
- *
139
- * - Builds the type-specific payload.
140
- * - POSTs with an `AbortSignal` timeout.
141
- * - On any error (network, non-2xx, timeout, exception), invokes
142
- * `config.onError(err, feedbackId)` if provided; otherwise logs a one-liner.
143
- */
144
- declare function dispatchWebhook(config: WebhookConfig, feedback: FeedbackRecord): Promise<void>;
145
- /**
146
- * Dispatch every configured webhook in parallel. Awaiting the returned promise
147
- * lets tests synchronize on completion, but production callers should drop the
148
- * promise on the floor (`void dispatchWebhooks(...)`) so the HTTP response
149
- * isn't held back on slow receivers.
150
- */
151
- declare function dispatchWebhooks(configs: readonly WebhookConfig[], feedback: FeedbackRecord): Promise<void>;
152
- /** One handler per HTTP method — mount them on any Fetch-API router. */
153
- interface SitepingHandler {
154
- OPTIONS: (request: Request) => Response;
155
- POST: (request: Request) => Promise<Response>;
156
- GET: (request: Request) => Promise<Response>;
157
- PATCH: (request: Request) => Promise<Response>;
158
- DELETE: (request: Request) => Promise<Response>;
159
- }
160
- interface FeedbackPatchInput {
161
- id: string;
162
- projectName: string;
163
- status: FeedbackStatus;
164
- }
165
- interface FeedbackDeleteSingle {
166
- id: string;
167
- projectName: string;
168
- }
169
- interface FeedbackDeleteAll {
170
- projectName: string;
171
- deleteAll: true;
172
- }
173
- type FeedbackDeleteInput = FeedbackDeleteSingle | FeedbackDeleteAll;
174
- interface GetQueryInput {
175
- projectName: string;
176
- /** Set to 1 by schema default when omitted from raw input. */
177
- page: number;
178
- /** Set to 50 by schema default when omitted from raw input. */
179
- limit: number;
180
- type?: FeedbackType | undefined;
181
- status?: FeedbackStatus | undefined;
182
- statuses?: FeedbackStatus[] | undefined;
183
- search?: string | undefined;
184
- url?: string | undefined;
185
- urlPattern?: string | undefined;
186
- }
1
+ import { FeedbackPayload, BeezpingStore, ScreenshotStorage, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord, FeedbackQuery, FeedbackPage, FeedbackUpdateInput } from './beezping-core.cjs';
2
+ export { BeezpingStore, CommentCreateInput, CommentPayload, FeedbackCreateInput, FeedbackRecord, ScreenshotStorage, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, StoreValueTooLongError, flattenAnnotation, isStorePersistence } from './beezping-core.cjs';
3
+ import { BeezpingApiKeyHandlerOptions, BeezpingPrincipal, BeezpingAccessHandlerOptions, BeezpingHandler } from '@beezping/server';
4
+ export { BeezpingAccessControl, BeezpingAction, BeezpingAuthorizationContext, BeezpingDeletionTarget, BeezpingHandler, BeezpingHandlerBaseOptions, BeezpingHttpMethod, BeezpingLifecycleHooks, BeezpingLogger, BeezpingPrincipal, BeezpingRequestContext, DiscordWebhookPayload, FeedbackDeleteInput, FeedbackPatchInput, GenericWebhookPayload, GetQueryInput, SlackWebhookPayload, WebhookConfig, WebhookPayloadMap, WebhookType, dispatchWebhook, dispatchWebhooks } from '@beezping/server';
187
5
 
188
6
  /**
189
7
  * @deprecated The create wire shape is core's `FeedbackPayload` — import
@@ -192,7 +10,7 @@ interface GetQueryInput {
192
10
  type FeedbackCreateSchemaInput = FeedbackPayload;
193
11
 
194
12
  /**
195
- * Structural type for a Prisma model delegate (`prisma.sitepingFeedback`).
13
+ * Structural type for a Prisma model delegate (`prisma.beezpingFeedback`).
196
14
  *
197
15
  * Arguments are kept `unknown` so any Prisma version's generated client
198
16
  * satisfies the constraint; the adapter assembles type-safe payloads
@@ -221,8 +39,15 @@ interface PrismaModelDelegate {
221
39
  * defines the subset of methods the adapter actually uses, so it can be
222
40
  * referenced in handler option types without importing `@prisma/client`.
223
41
  */
224
- interface SitepingPrismaClient {
225
- sitepingFeedback: PrismaModelDelegate;
42
+ interface BeezpingPrismaClient {
43
+ beezpingFeedback: PrismaModelDelegate;
44
+ /**
45
+ * Generated once the schema declares the `BeezpingComment` model
46
+ * (`npx @beezping/cli sync`). Optional, so a client generated from an older
47
+ * schema keeps type-checking: `PrismaStore` then has no threads and the
48
+ * handler answers comment writes with 501.
49
+ */
50
+ beezpingComment?: PrismaModelDelegate | undefined;
226
51
  }
227
52
  /**
228
53
  * Options accepted by `PrismaStore`.
@@ -253,7 +78,7 @@ interface PrismaStoreOptions {
253
78
  screenshotStorage?: ScreenshotStorage | undefined;
254
79
  }
255
80
  /**
256
- * Prisma-backed implementation of `SitepingStore`.
81
+ * Prisma-backed implementation of `BeezpingStore`.
257
82
  *
258
83
  * Wraps a PrismaClient to satisfy the abstract store interface.
259
84
  *
@@ -262,7 +87,7 @@ interface PrismaStoreOptions {
262
87
  * the database stays small. Without `screenshotStorage`, the data URL is
263
88
  * persisted inline (logged once on first use as a heads-up).
264
89
  */
265
- declare class PrismaStore implements SitepingStore {
90
+ declare class PrismaStore implements BeezpingStore {
266
91
  /** @internal */
267
92
  private prisma;
268
93
  private readonly screenshotStorage;
@@ -270,8 +95,31 @@ declare class PrismaStore implements SitepingStore {
270
95
  private inlineFallbackWarned;
271
96
  /** @internal */
272
97
  private caseInsensitiveSearch;
273
- constructor(prisma: SitepingPrismaClient, options?: PrismaStoreOptions);
274
- createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord$1>;
98
+ /** Read shape of every feedback query — with the thread once the client has the comment model. */
99
+ private readonly include;
100
+ /**
101
+ * Add a comment to a feedback's thread — defined only when the client has
102
+ * the `BeezpingComment` delegate, unless a subclass defines its own. Without
103
+ * it the store has no threads, and the handler answers comment writes with
104
+ * 501 instead of every post failing with a Prisma error. Declared, not a
105
+ * field: a field would set it on every instance, hiding a subclass's method.
106
+ */
107
+ readonly addComment?: (feedbackId: string, data: CommentCreateInput) => Promise<CommentRecord>;
108
+ /** Delete one comment from a feedback's thread — defined under the same condition as {@link addComment}. */
109
+ readonly deleteComment?: (feedbackId: string, commentId: string) => Promise<void>;
110
+ constructor(prisma: BeezpingPrismaClient, options?: PrismaStoreOptions);
111
+ createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;
112
+ /**
113
+ * Drop the screenshot uploaded by a replay of a stored clientId — unless the
114
+ * stored row references it. Uploads are keyed on clientId, so with a
115
+ * deterministic key (`feedback/${feedbackId}.jpg`) the replay wrote to the
116
+ * very object the existing row points at: deleting it would strip the
117
+ * surviving feedback of its screenshot. If the lookup itself fails the
118
+ * object is kept — an orphan beats data loss. Other insert failures never
119
+ * get here: with such a key, a retry on another instance may have rewritten
120
+ * the object and not yet inserted the row that will point at it.
121
+ */
122
+ private discardUnreferencedUpload;
275
123
  private insertFeedback;
276
124
  /**
277
125
  * Resolve the value to persist on `Feedback.screenshotUrl`.
@@ -296,13 +144,34 @@ declare class PrismaStore implements SitepingStore {
296
144
  * logged and swallowed: an orphaned object is preferable to a delete that
297
145
  * reports failure after the row is already gone. Inline `data:` URLs and
298
146
  * stores without a `delete` hook are skipped.
147
+ *
148
+ * Deletes run through a pool of at most {@link SCREENSHOT_DELETE_CONCURRENCY}
149
+ * concurrent calls: a project delete may free thousands of objects, and one
150
+ * socket each at once would exhaust the file descriptors of a serverless
151
+ * function, orphaning most of them.
299
152
  */
300
153
  private discardScreenshots;
301
154
  /** URLs of the stored screenshots in `projectName` — only fetched when a `delete` hook can use them. */
302
155
  private storedScreenshotUrls;
303
- findByClientId(clientId: string): Promise<FeedbackRecord$1 | null>;
156
+ findByClientId(clientId: string): Promise<FeedbackRecord | null>;
304
157
  getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;
305
- updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord$1>;
158
+ updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord>;
159
+ /**
160
+ * Insert a comment. A replayed `clientId` returns the stored comment, looked
161
+ * up first so a replay never runs into the thread cap; a concurrent replay
162
+ * that wins the insert race surfaces as P2002 and is read back the same
163
+ * way. The `connect` turns a missing feedback into Prisma's P2025 on every
164
+ * provider, rather than each database's own foreign-key error. The cap on
165
+ * `client` comments is a count before the insert, so posts racing for the
166
+ * last free slot may overshoot it; a `team` comment skips it.
167
+ */
168
+ private insertComment;
169
+ private findComment;
170
+ /**
171
+ * Delete one comment. Both ids go in one `deleteMany`, so a comment of
172
+ * another thread and an unknown one are the same zero-row miss.
173
+ */
174
+ private removeComment;
306
175
  deleteFeedback(id: string): Promise<void>;
307
176
  deleteAllFeedbacks(projectName: string): Promise<void>;
308
177
  /**
@@ -311,11 +180,12 @@ declare class PrismaStore implements SitepingStore {
311
180
  */
312
181
  verifyProjectOwnership(id: string, projectName: string): Promise<boolean>;
313
182
  }
314
- interface HandlerOptions extends ApiKeyAccessOptions {
183
+ /** How the handler reaches its data: a Prisma client, or any store. */
184
+ interface PrismaHandlerStoreOptions {
315
185
  /** Prisma client — used when `store` is not provided. Wrapped in a `PrismaStore` internally. */
316
- prisma?: SitepingPrismaClient;
186
+ prisma?: BeezpingPrismaClient;
317
187
  /** Abstract store — when provided, takes precedence over `prisma`. */
318
- store?: SitepingStore;
188
+ store?: BeezpingStore;
319
189
  /**
320
190
  * Optional storage backend for screenshots. Used only with `prisma`
321
191
  * (ignored when a custom `store` is passed — that store is responsible
@@ -323,8 +193,6 @@ interface HandlerOptions extends ApiKeyAccessOptions {
323
193
  * persisted inline on `Feedback.screenshotUrl` with a one-time warn.
324
194
  */
325
195
  screenshotStorage?: ScreenshotStorage;
326
- /** Allowed CORS origins — when set, validates the Origin header */
327
- allowedOrigins?: ReadonlyArray<string> | undefined;
328
196
  /**
329
197
  * Override case-insensitive search behaviour for the built-in `PrismaStore`.
330
198
  *
@@ -333,31 +201,45 @@ interface HandlerOptions extends ApiKeyAccessOptions {
333
201
  * auto-detection and per-provider semantics.
334
202
  */
335
203
  caseInsensitiveSearch?: boolean;
336
- /**
337
- * Outgoing webhooks fired after a feedback is successfully persisted.
338
- * Fire-and-forget; provide `onError` on each config to observe failures.
339
- */
340
- webhooks?: WebhookConfig | ReadonlyArray<WebhookConfig>;
204
+ }
205
+ /** Options of `createBeezpingHandler` under the `apiKey` policy — every `@beezping/server` option. */
206
+ interface HandlerOptions extends Omit<BeezpingApiKeyHandlerOptions, "store">, PrismaHandlerStoreOptions {
207
+ }
208
+ /** Options of `createBeezpingHandler` under a custom `access` policy (see `@beezping/server`). */
209
+ interface PrismaAccessHandlerOptions<Principal extends BeezpingPrincipal> extends Omit<BeezpingAccessHandlerOptions<Principal>, "store">, PrismaHandlerStoreOptions {
341
210
  }
342
211
  /**
343
- * Create request handlers for the Siteping API endpoint.
212
+ * Create request handlers for the Beezping API endpoint — `@beezping/server`'s
213
+ * `createBeezpingHandler` over a Prisma-backed store, with every server option.
344
214
  *
345
215
  * Accepts either a `store` (abstract) or a `prisma` client (backwards compatible).
346
216
  * When `prisma` is provided without `store`, it is wrapped in a `PrismaStore`.
347
- * For custom auth (sessions, roles), lifecycle hooks or input transforms use
348
- * `createSitepingHandler` from `@beezping/server` with a `PrismaStore`.
349
217
  *
350
218
  * **Rate limiting** is not handled by this library. Apply rate limiting at the
351
219
  * framework or reverse-proxy level (e.g. Next.js middleware, Nginx, Cloudflare).
220
+ * The POST endpoint in particular should be rate-limited to prevent abuse, since
221
+ * the widget typically calls it from unauthenticated browser contexts.
222
+ *
223
+ * @example Next.js App Router — `app/api/beezping/route.ts`
224
+ * ```ts
225
+ * import { createBeezpingHandler } from '@beezping/adapter-prisma'
226
+ * import { prisma } from '@/lib/prisma'
227
+ *
228
+ * export const { GET, POST, PATCH, DELETE, OPTIONS } = createBeezpingHandler({ prisma })
229
+ * ```
352
230
  *
353
- * @example Next.js App Router — `app/api/siteping/route.ts`
231
+ * @example With abstract store
354
232
  * ```ts
355
- * import { createSitepingHandler } from '@beezping/adapter-prisma'
233
+ * import { createBeezpingHandler, PrismaStore } from '@beezping/adapter-prisma'
356
234
  * import { prisma } from '@/lib/prisma'
357
235
  *
358
- * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ prisma })
236
+ * const store = new PrismaStore(prisma)
237
+ * export const { GET, POST, PATCH, DELETE, OPTIONS } = createBeezpingHandler({ store })
359
238
  * ```
360
239
  */
361
- declare function createSitepingHandler({ prisma, store: providedStore, screenshotStorage, caseInsensitiveSearch, ...handlerOptions }: HandlerOptions): SitepingHandler;
240
+ declare function createBeezpingHandler<Principal extends BeezpingPrincipal>(options: PrismaAccessHandlerOptions<Principal>): BeezpingHandler;
241
+ declare function createBeezpingHandler(options: HandlerOptions): BeezpingHandler;
242
+ /** Options assembled at runtime, either policy. */
243
+ declare function createBeezpingHandler<Principal extends BeezpingPrincipal>(options: HandlerOptions | PrismaAccessHandlerOptions<Principal>): BeezpingHandler;
362
244
 
363
- export { type DiscordWebhookPayload, type FeedbackCreateSchemaInput, type FeedbackDeleteInput, type FeedbackPatchInput, type GenericWebhookPayload, type GetQueryInput, type HandlerOptions, type PrismaModelDelegate, PrismaStore, type PrismaStoreOptions, type SitepingHandler, type SitepingHttpMethod, type SitepingPrismaClient, type SlackWebhookPayload, type WebhookConfig, type WebhookPayloadMap, type WebhookType, createSitepingHandler, dispatchWebhook, dispatchWebhooks };
245
+ export { type BeezpingPrismaClient, type FeedbackCreateSchemaInput, type HandlerOptions, type PrismaAccessHandlerOptions, type PrismaModelDelegate, PrismaStore, type PrismaStoreOptions, createBeezpingHandler };