@siteping/adapter-drizzle 0.1.0 → 0.2.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/libsql.d.cts CHANGED
@@ -3,7 +3,7 @@ export { DEFAULT_SITEPING_TABLE_NAMES, DrizzleStoreLogger } from './index.cjs';
3
3
  import { LibSQLDatabase } from 'drizzle-orm/libsql';
4
4
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
5
5
  import { ScreenshotRegion, DiagnosticsSnapshot } from './siteping-core.cjs';
6
- export { FeedbackRecord, ScreenshotStorage, SitepingStore, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, isStorePersistence } from './siteping-core.cjs';
6
+ export { FeedbackRecord, ScreenshotStorage, SitepingStore, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, StoreValueTooLongError, isStoreDuplicate, isStoreLimit, isStoreNotFound, isStorePersistence, isStoreValueTooLong } from './siteping-core.cjs';
7
7
 
8
8
  /**
9
9
  * Build the SitePing tables for SQLite / libSQL (Turso). Export them from
@@ -1012,4 +1012,5 @@ interface LibSQLSitepingStoreOptions extends DrizzleStoreOptions {
1012
1012
  */
1013
1013
  declare function createLibSQLSitepingStore(db: AnyLibSQLDatabase, options?: LibSQLSitepingStoreOptions): DrizzleStore;
1014
1014
 
1015
- export { type AnyLibSQLDatabase, DrizzleStore, DrizzleStoreOptions, type LibSQLSitepingStoreOptions, type SitepingSqliteTables, SitepingTableNames, createLibSQLSitepingStore, createSitepingSqliteTables };
1015
+ export { DrizzleStore, DrizzleStoreOptions, SitepingTableNames, createLibSQLSitepingStore, createSitepingSqliteTables };
1016
+ export type { AnyLibSQLDatabase, LibSQLSitepingStoreOptions, SitepingSqliteTables };
package/dist/libsql.d.ts CHANGED
@@ -3,7 +3,7 @@ export { DEFAULT_SITEPING_TABLE_NAMES, DrizzleStoreLogger } from './index.js';
3
3
  import { LibSQLDatabase } from 'drizzle-orm/libsql';
4
4
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
5
5
  import { ScreenshotRegion, DiagnosticsSnapshot } from './siteping-core.js';
6
- export { FeedbackRecord, ScreenshotStorage, SitepingStore, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, isStorePersistence } from './siteping-core.js';
6
+ export { FeedbackRecord, ScreenshotStorage, SitepingStore, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, StoreValueTooLongError, isStoreDuplicate, isStoreLimit, isStoreNotFound, isStorePersistence, isStoreValueTooLong } from './siteping-core.js';
7
7
 
8
8
  /**
9
9
  * Build the SitePing tables for SQLite / libSQL (Turso). Export them from
@@ -1012,4 +1012,5 @@ interface LibSQLSitepingStoreOptions extends DrizzleStoreOptions {
1012
1012
  */
1013
1013
  declare function createLibSQLSitepingStore(db: AnyLibSQLDatabase, options?: LibSQLSitepingStoreOptions): DrizzleStore;
1014
1014
 
1015
- export { type AnyLibSQLDatabase, DrizzleStore, DrizzleStoreOptions, type LibSQLSitepingStoreOptions, type SitepingSqliteTables, SitepingTableNames, createLibSQLSitepingStore, createSitepingSqliteTables };
1015
+ export { DrizzleStore, DrizzleStoreOptions, SitepingTableNames, createLibSQLSitepingStore, createSitepingSqliteTables };
1016
+ export type { AnyLibSQLDatabase, LibSQLSitepingStoreOptions, SitepingSqliteTables };
package/dist/libsql.js CHANGED
@@ -7,11 +7,16 @@ import {
7
7
  StoreLimitError,
8
8
  StoreNotFoundError,
9
9
  StorePersistenceError,
10
+ StoreValueTooLongError,
10
11
  buildFeedbackWhere,
11
12
  deletedFeedbackColumns,
12
13
  feedbackRecordColumns,
13
14
  guardedCommentInsert,
15
+ isStoreDuplicate,
16
+ isStoreLimit,
17
+ isStoreNotFound,
14
18
  isStorePersistence,
19
+ isStoreValueTooLong,
15
20
  monotonicUpdatedAt,
16
21
  newestFeedbackFirst,
17
22
  primaryOf,
@@ -19,7 +24,7 @@ import {
19
24
  selectValues,
20
25
  toDeletedFeedbacks,
21
26
  withSearchableMessage
22
- } from "./chunk-6G6UAERF.js";
27
+ } from "./chunk-6DO75JHR.js";
23
28
  import {
24
29
  DEFAULT_SITEPING_TABLE_NAMES
25
30
  } from "./chunk-ICY2AW6S.js";
@@ -247,8 +252,13 @@ export {
247
252
  StoreLimitError,
248
253
  StoreNotFoundError,
249
254
  StorePersistenceError,
255
+ StoreValueTooLongError,
250
256
  createLibSQLSitepingStore,
251
257
  createSitepingSqliteTables,
252
- isStorePersistence
258
+ isStoreDuplicate,
259
+ isStoreLimit,
260
+ isStoreNotFound,
261
+ isStorePersistence,
262
+ isStoreValueTooLong
253
263
  };
254
264
  //# sourceMappingURL=libsql.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/libsql/index.ts","../src/libsql/gateway.ts","../src/libsql/tables.ts"],"sourcesContent":["import { is } from \"drizzle-orm\";\nimport { LibSQLDatabase } from \"drizzle-orm/libsql/driver-core\";\nimport { DRIZZLE_STORE_MESSAGE_PREFIX } from \"../constants/errors.js\";\nimport { primaryOf } from \"../shared/replicas.js\";\nimport { DrizzleSitepingStore, type DrizzleStore, type DrizzleStoreOptions } from \"../shared/store.js\";\nimport { type AnyLibSQLDatabase, createLibSQLGateway } from \"./gateway.js\";\nimport { createSitepingSqliteTables, type SitepingSqliteTables } from \"./tables.js\";\n\nexport type { FeedbackRecord, ScreenshotStorage, SitepingStore } from \"@siteping/core\";\nexport {\n isStorePersistence,\n StoreDuplicateError,\n StoreLimitError,\n StoreNotFoundError,\n StorePersistenceError,\n} from \"@siteping/core\";\nexport { DEFAULT_SITEPING_TABLE_NAMES, type SitepingTableNames } from \"../constants/table-names.js\";\nexport type { DrizzleStore, DrizzleStoreLogger, DrizzleStoreOptions } from \"../shared/store.js\";\nexport type { AnyLibSQLDatabase } from \"./gateway.js\";\nexport { createSitepingSqliteTables, type SitepingSqliteTables } from \"./tables.js\";\n\nexport interface LibSQLSitepingStoreOptions extends DrizzleStoreOptions {\n /** Tables built with `createSitepingSqliteTables` — pass them when you customized the names. */\n tables?: SitepingSqliteTables | undefined;\n}\n\n/**\n * `SitepingStore` on Turso / libSQL through Drizzle ORM. Given a database\n * built with `withReplicas`, it runs everything on the primary.\n *\n * @throws `TypeError` when `db` does not come from `drizzle-orm/libsql`.\n * Another SQLite driver's database (Cloudflare D1…) passes the type check\n * where `@libsql/client`'s types do not resolve, and would fail only at run\n * time, on the data: D1 binds at most 100 parameters per query, and a\n * feedback with 5 annotations binds more.\n *\n * @example\n * ```ts\n * import { drizzle } from \"drizzle-orm/libsql\";\n * import { createLibSQLSitepingStore } from \"@siteping/adapter-drizzle/libsql\";\n *\n * const db = drizzle({ connection: { url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN! } });\n * const store = createLibSQLSitepingStore(db, { screenshotStorage });\n * ```\n */\nexport function createLibSQLSitepingStore(\n db: AnyLibSQLDatabase,\n options: LibSQLSitepingStoreOptions = {},\n): DrizzleStore {\n const primary = primaryOf(db);\n if (!is(primary, LibSQLDatabase)) {\n throw new TypeError(\n `${DRIZZLE_STORE_MESSAGE_PREFIX}: createLibSQLSitepingStore needs a database from drizzle-orm/libsql — other SQLite drivers (Cloudflare D1, better-sqlite3…) are not supported`,\n );\n }\n const { tables = createSitepingSqliteTables(), ...storeOptions } = options;\n return new DrizzleSitepingStore(createLibSQLGateway(primary, tables), storeOptions);\n}\n","import { and, count, eq, getTableColumns, inArray, sql } from \"drizzle-orm\";\nimport type { LibSQLDatabase } from \"drizzle-orm/libsql\";\nimport { CASE_INSENSITIVE_LIKE_OPERATOR } from \"../constants/search.js\";\nimport { GREATEST_VALUE_FUNCTION } from \"../constants/sql.js\";\nimport { guardedCommentInsert } from \"../shared/comments.js\";\nimport { deletedFeedbackColumns, toDeletedFeedbacks } from \"../shared/deletes.js\";\nimport { feedbackRecordColumns, newestFeedbackFirst } from \"../shared/feedbacks.js\";\nimport { buildFeedbackWhere, withSearchableMessage } from \"../shared/filters.js\";\nimport type { FeedbackFilter, SitepingSqlGateway } from \"../shared/gateway.js\";\nimport { recordColumns, selectValues } from \"../shared/rows.js\";\nimport { monotonicUpdatedAt } from \"../shared/timestamps.js\";\nimport type { SitepingSqliteTables } from \"./tables.js\";\n\n/** Any Drizzle libSQL database — Turso (remote / embedded replica) or a local libSQL/SQLite file. */\n// biome-ignore lint/suspicious/noExplicitAny: accepts every schema generic.\nexport type AnyLibSQLDatabase = LibSQLDatabase<any>;\n\n/** The libSQL SQL behind `createLibSQLSitepingStore`. */\nexport function createLibSQLGateway(\n db: AnyLibSQLDatabase,\n { sitepingFeedbacks, sitepingAnnotations, sitepingComments }: SitepingSqliteTables,\n): SitepingSqlGateway {\n const whereClause = (filter: FeedbackFilter) =>\n buildFeedbackWhere(sitepingFeedbacks, filter, CASE_INSENSITIVE_LIKE_OPERATOR.sqlite);\n const feedbackColumns = feedbackRecordColumns(getTableColumns(sitepingFeedbacks));\n const commentColumns = recordColumns(getTableColumns(sitepingComments));\n /** Number of feedback rows matching a where clause — the page `total`. */\n const countMatching = async (where: ReturnType<typeof whereClause>): Promise<number> => {\n const [totals] = await db.select({ total: count() }).from(sitepingFeedbacks).where(where);\n return totals?.total ?? 0;\n };\n // SQLite's implicit rowid is the insertion ordinal: the database assigns it on\n // every insert — the store's, or the host application's through the exported\n // table — under its single-writer lock, above every existing row's rowid\n // (without AUTOINCREMENT a deleted maximum may be reused, which still ranks\n // the new row after every row that remains).\n const insertionOrder = sql`${sitepingFeedbacks}.rowid`;\n\n // Multi-statement writes go through `db.batch`, never an interactive\n // `db.transaction`: libSQL runs a batch as one transaction without yielding\n // between its statements, so no write lock is held across an `await` and\n // the host application's own writes on the same database never hit\n // SQLITE_BUSY because of the store.\n return {\n async insertFeedback(feedback, annotations) {\n const insertFeedbackRow = db\n .insert(sitepingFeedbacks)\n .values(withSearchableMessage(feedback))\n .onConflictDoNothing({ target: sitepingFeedbacks.clientId })\n .returning({ id: sitepingFeedbacks.id });\n if (annotations.length === 0) return (await insertFeedbackRow).length > 0;\n\n // The feedback id is fresh, so it exists only when this batch inserted it.\n const [inserted] = await db.batch([\n insertFeedbackRow,\n db.insert(sitepingAnnotations).select(\n selectValues(\n sitepingAnnotations,\n annotations.map((annotation, position) => ({ ...annotation, position })),\n sql`EXISTS (SELECT 1 FROM ${sitepingFeedbacks} WHERE ${sitepingFeedbacks.id} = ${feedback.id})`,\n ),\n ),\n ]);\n return inserted.length > 0;\n },\n async findFeedbacks(filter, { limit, offset }) {\n const where = whereClause(filter);\n const [rows, total] = await Promise.all([\n db\n .select(feedbackColumns)\n .from(sitepingFeedbacks)\n .where(where)\n .orderBy(...newestFeedbackFirst(sitepingFeedbacks.createdAt, insertionOrder))\n .limit(limit)\n .offset(offset),\n countMatching(where),\n ]);\n return { rows, total };\n },\n async countFeedbacks(filter) {\n return countMatching(whereClause(filter));\n },\n async findAnnotations(feedbackIds) {\n return db\n .select(recordColumns(getTableColumns(sitepingAnnotations)))\n .from(sitepingAnnotations)\n .where(inArray(sitepingAnnotations.feedbackId, [...feedbackIds]))\n .orderBy(sitepingAnnotations.createdAt, sitepingAnnotations.position);\n },\n async findComments(feedbackIds) {\n return db\n .select(commentColumns)\n .from(sitepingComments)\n .where(inArray(sitepingComments.feedbackId, [...feedbackIds]))\n .orderBy(sitepingComments.createdAt, sitepingComments.position);\n },\n async findCommentByClientId(clientId) {\n const [row] = await db\n .select(commentColumns)\n .from(sitepingComments)\n .where(eq(sitepingComments.clientId, clientId))\n .limit(1);\n return row ?? null;\n },\n async insertComment(comment, maxComments) {\n const { values, condition } = guardedCommentInsert(\n { feedbacks: sitepingFeedbacks, comments: sitepingComments },\n comment,\n maxComments,\n );\n const inserted = await db\n .insert(sitepingComments)\n .select(selectValues(sitepingComments, [values], condition))\n .onConflictDoNothing({ target: sitepingComments.clientId })\n .returning({ id: sitepingComments.id });\n return inserted.length > 0;\n },\n async deleteComment(feedbackId, commentId) {\n const deleted = await db\n .delete(sitepingComments)\n .where(and(eq(sitepingComments.id, commentId), eq(sitepingComments.feedbackId, feedbackId)))\n .returning({ id: sitepingComments.id });\n return deleted.length > 0;\n },\n async findByClientId(clientId) {\n const [row] = await db\n .select(feedbackColumns)\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.clientId, clientId))\n .limit(1);\n return row ?? null;\n },\n async findProjectName(id) {\n const [row] = await db\n .select({ projectName: sitepingFeedbacks.projectName })\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.id, id))\n .limit(1);\n return row?.projectName ?? null;\n },\n async updateStatus(id, { status, resolvedAt, updatedAt }) {\n const [row] = await db\n .update(sitepingFeedbacks)\n .set({\n status,\n resolvedAt,\n updatedAt: monotonicUpdatedAt(sitepingFeedbacks, updatedAt, GREATEST_VALUE_FUNCTION.sqlite),\n })\n .where(eq(sitepingFeedbacks.id, id))\n .returning(feedbackColumns);\n return row ?? null;\n },\n async deleteById(id, options) {\n // Annotations and comments cascade only with `PRAGMA foreign_keys = ON`,\n // which libSQL does not guarantee — delete them explicitly in the same batch.\n const [, , deleted] = await db.batch([\n db.delete(sitepingAnnotations).where(eq(sitepingAnnotations.feedbackId, id)),\n db.delete(sitepingComments).where(eq(sitepingComments.feedbackId, id)),\n db\n .delete(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.id, id))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, options)),\n ]);\n return deleted.length > 0 ? toDeletedFeedbacks(deleted) : null;\n },\n async deleteByProject(projectName) {\n const projectFeedbackIds = db\n .select({ id: sitepingFeedbacks.id })\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.projectName, projectName));\n await db.batch([\n db.delete(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, projectFeedbackIds)),\n db.delete(sitepingComments).where(inArray(sitepingComments.feedbackId, projectFeedbackIds)),\n db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName)),\n ]);\n },\n async deleteProjectChunk(projectName, chunkSize) {\n // Ordered by the primary key, every statement of the batch picks the same\n // rows: nothing else writes the feedback table between them.\n const chunkIds = db\n .select({ id: sitepingFeedbacks.id })\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.projectName, projectName))\n .orderBy(sitepingFeedbacks.id)\n .limit(chunkSize);\n const [, , deleted] = await db.batch([\n db.delete(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, chunkIds)),\n db.delete(sitepingComments).where(inArray(sitepingComments.feedbackId, chunkIds)),\n db\n .delete(sitepingFeedbacks)\n .where(inArray(sitepingFeedbacks.id, chunkIds))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, { collectScreenshotUrls: true })),\n ]);\n return toDeletedFeedbacks(deleted);\n },\n async findReferencedScreenshotUrls(screenshotUrls) {\n const rows = await db\n .selectDistinct({ screenshotUrl: sitepingFeedbacks.screenshotUrl })\n .from(sitepingFeedbacks)\n .where(inArray(sitepingFeedbacks.screenshotUrl, [...screenshotUrls]));\n return new Set(rows.flatMap((row) => (row.screenshotUrl === null ? [] : [row.screenshotUrl])));\n },\n };\n}\n","import type {\n CommentAuthorRole,\n DiagnosticsSnapshot,\n FeedbackStatus,\n FeedbackType,\n ScreenshotRegion,\n} from \"@siteping/core\";\nimport { index, integer, real, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\nimport { DEFAULT_SITEPING_TABLE_NAMES, type SitepingTableNames } from \"../constants/table-names.js\";\n\n/**\n * Build the SitePing tables for SQLite / libSQL (Turso). Export them from\n * your Drizzle schema so `drizzle-kit generate` / `push` creates them:\n *\n * ```ts\n * // db/schema.ts\n * export const { sitepingFeedbacks, sitepingAnnotations, sitepingComments } = createSitepingSqliteTables();\n * ```\n *\n * JSON columns are stored as text and timestamps as epoch milliseconds. The\n * feedback table is an ordinary rowid table: its implicit `rowid`, which the\n * database assigns on every insert, breaks `createdAt` ties in the \"newest\n * first\" ordering — never redeclare it `WITHOUT ROWID`.\n */\nexport function createSitepingSqliteTables(names: SitepingTableNames = DEFAULT_SITEPING_TABLE_NAMES) {\n const sitepingFeedbacks = sqliteTable(\n names.feedbacks,\n {\n id: text(\"id\").primaryKey(),\n projectName: text(\"project_name\").notNull(),\n type: text(\"type\").$type<FeedbackType>().notNull(),\n message: text(\"message\").notNull(),\n status: text(\"status\").$type<FeedbackStatus>().notNull().default(\"open\"),\n url: text(\"url\").notNull(),\n urlPattern: text(\"url_pattern\"),\n screenshotUrl: text(\"screenshot_url\"),\n screenshotRegion: text(\"screenshot_region\", { mode: \"json\" }).$type<ScreenshotRegion>(),\n diagnostics: text(\"diagnostics\", { mode: \"json\" }).$type<DiagnosticsSnapshot>(),\n viewport: text(\"viewport\").notNull(),\n userAgent: text(\"user_agent\").notNull(),\n authorName: text(\"author_name\").notNull(),\n authorEmail: text(\"author_email\").notNull(),\n clientId: text(\"client_id\").notNull(),\n resolvedAt: integer(\"resolved_at\", { mode: \"timestamp_ms\" }),\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" }).notNull(),\n updatedAt: integer(\"updated_at\", { mode: \"timestamp_ms\" }).notNull(),\n // `message` lowercased in JavaScript (`String.prototype.toLowerCase`, Unicode-aware), the\n // column the text search reads: SQLite's LIKE folds only ASCII case, so `Échec` would not\n // match `échec`. The store fills it on insert (a feedback's message never changes); rows\n // written by the host application stay NULL and the search falls back to `message` with\n // ASCII-only folding. Internal — never part of the feedback record.\n messageSearch: text(\"message_search\"),\n },\n (table) => [\n uniqueIndex(`${names.feedbacks}_client_id_key`).on(table.clientId),\n index(`${names.feedbacks}_project_status_created_idx`).on(table.projectName, table.status, table.createdAt),\n index(`${names.feedbacks}_project_url_idx`).on(table.projectName, table.url),\n ],\n );\n\n const sitepingAnnotations = sqliteTable(\n names.annotations,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\")\n .notNull()\n .references(() => sitepingFeedbacks.id, { onDelete: \"cascade\" }),\n cssSelector: text(\"css_selector\").notNull(),\n xpath: text(\"xpath\").notNull(),\n textSnippet: text(\"text_snippet\").notNull(),\n elementTag: text(\"element_tag\").notNull(),\n elementId: text(\"element_id\"),\n textPrefix: text(\"text_prefix\").notNull(),\n textSuffix: text(\"text_suffix\").notNull(),\n fingerprint: text(\"fingerprint\").notNull(),\n neighborText: text(\"neighbor_text\").notNull(),\n anchorKey: text(\"anchor_key\"),\n xPct: real(\"x_pct\").notNull(),\n yPct: real(\"y_pct\").notNull(),\n wPct: real(\"w_pct\").notNull(),\n hPct: real(\"h_pct\").notNull(),\n scrollX: real(\"scroll_x\").notNull(),\n scrollY: real(\"scroll_y\").notNull(),\n viewportW: integer(\"viewport_w\").notNull(),\n viewportH: integer(\"viewport_h\").notNull(),\n devicePixelRatio: real(\"device_pixel_ratio\").notNull().default(1),\n // Submission index within the feedback: `annotations[0]` is the primary anchor, and\n // every annotation of a feedback shares one `createdAt`. Rows the host application\n // inserts without it default to 0 and keep their `createdAt` order.\n position: integer(\"position\").notNull().default(0),\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" }).notNull(),\n },\n (table) => [index(`${names.annotations}_feedback_id_idx`).on(table.feedbackId)],\n );\n\n const sitepingComments = sqliteTable(\n names.comments,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\")\n .notNull()\n .references(() => sitepingFeedbacks.id, { onDelete: \"cascade\" }),\n body: text(\"body\").notNull(),\n authorName: text(\"author_name\").notNull(),\n authorEmail: text(\"author_email\").notNull(),\n authorRole: text(\"author_role\").$type<CommentAuthorRole>().notNull().default(\"client\"),\n clientId: text(\"client_id\").notNull(),\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" }).notNull(),\n // Posting index within the thread, assigned by the insert itself: breaks `createdAt`\n // ties so a thread reads in posting order. Rows the host application inserts without\n // it default to 0 and keep their `createdAt` order.\n position: integer(\"position\").notNull().default(0),\n },\n (table) => [\n uniqueIndex(`${names.comments}_client_id_key`).on(table.clientId),\n index(`${names.comments}_feedback_created_idx`).on(table.feedbackId, table.createdAt),\n ],\n );\n\n return { sitepingFeedbacks, sitepingAnnotations, sitepingComments };\n}\n\nexport type SitepingSqliteTables = ReturnType<typeof createSitepingSqliteTables>;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/libsql/index.ts","../src/libsql/gateway.ts","../src/libsql/tables.ts"],"sourcesContent":["import { is } from \"drizzle-orm\";\nimport { LibSQLDatabase } from \"drizzle-orm/libsql/driver-core\";\nimport { DRIZZLE_STORE_MESSAGE_PREFIX } from \"../constants/errors.js\";\nimport { primaryOf } from \"../shared/replicas.js\";\nimport { DrizzleSitepingStore, type DrizzleStore, type DrizzleStoreOptions } from \"../shared/store.js\";\nimport { type AnyLibSQLDatabase, createLibSQLGateway } from \"./gateway.js\";\nimport { createSitepingSqliteTables, type SitepingSqliteTables } from \"./tables.js\";\n\nexport type { FeedbackRecord, ScreenshotStorage, SitepingStore } from \"@siteping/core\";\nexport {\n isStoreDuplicate,\n isStoreLimit,\n isStoreNotFound,\n isStorePersistence,\n isStoreValueTooLong,\n StoreDuplicateError,\n StoreLimitError,\n StoreNotFoundError,\n StorePersistenceError,\n StoreValueTooLongError,\n} from \"@siteping/core\";\nexport { DEFAULT_SITEPING_TABLE_NAMES, type SitepingTableNames } from \"../constants/table-names.js\";\nexport type { DrizzleStore, DrizzleStoreLogger, DrizzleStoreOptions } from \"../shared/store.js\";\nexport type { AnyLibSQLDatabase } from \"./gateway.js\";\nexport { createSitepingSqliteTables, type SitepingSqliteTables } from \"./tables.js\";\n\nexport interface LibSQLSitepingStoreOptions extends DrizzleStoreOptions {\n /** Tables built with `createSitepingSqliteTables` — pass them when you customized the names. */\n tables?: SitepingSqliteTables | undefined;\n}\n\n/**\n * `SitepingStore` on Turso / libSQL through Drizzle ORM. Given a database\n * built with `withReplicas`, it runs everything on the primary.\n *\n * @throws `TypeError` when `db` does not come from `drizzle-orm/libsql`.\n * Another SQLite driver's database (Cloudflare D1…) passes the type check\n * where `@libsql/client`'s types do not resolve, and would fail only at run\n * time, on the data: D1 binds at most 100 parameters per query, and a\n * feedback with 5 annotations binds more.\n *\n * @example\n * ```ts\n * import { drizzle } from \"drizzle-orm/libsql\";\n * import { createLibSQLSitepingStore } from \"@siteping/adapter-drizzle/libsql\";\n *\n * const db = drizzle({ connection: { url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN! } });\n * const store = createLibSQLSitepingStore(db, { screenshotStorage });\n * ```\n */\nexport function createLibSQLSitepingStore(\n db: AnyLibSQLDatabase,\n options: LibSQLSitepingStoreOptions = {},\n): DrizzleStore {\n const primary = primaryOf(db);\n if (!is(primary, LibSQLDatabase)) {\n throw new TypeError(\n `${DRIZZLE_STORE_MESSAGE_PREFIX}: createLibSQLSitepingStore needs a database from drizzle-orm/libsql — other SQLite drivers (Cloudflare D1, better-sqlite3…) are not supported`,\n );\n }\n const { tables = createSitepingSqliteTables(), ...storeOptions } = options;\n return new DrizzleSitepingStore(createLibSQLGateway(primary, tables), storeOptions);\n}\n","import { and, count, eq, getTableColumns, inArray, sql } from \"drizzle-orm\";\nimport type { LibSQLDatabase } from \"drizzle-orm/libsql\";\nimport { CASE_INSENSITIVE_LIKE_OPERATOR } from \"../constants/search.js\";\nimport { GREATEST_VALUE_FUNCTION } from \"../constants/sql.js\";\nimport { guardedCommentInsert } from \"../shared/comments.js\";\nimport { deletedFeedbackColumns, toDeletedFeedbacks } from \"../shared/deletes.js\";\nimport { feedbackRecordColumns, newestFeedbackFirst } from \"../shared/feedbacks.js\";\nimport { buildFeedbackWhere, withSearchableMessage } from \"../shared/filters.js\";\nimport type { FeedbackFilter, SitepingSqlGateway } from \"../shared/gateway.js\";\nimport { recordColumns, selectValues } from \"../shared/rows.js\";\nimport { monotonicUpdatedAt } from \"../shared/timestamps.js\";\nimport type { SitepingSqliteTables } from \"./tables.js\";\n\n/** Any Drizzle libSQL database — Turso (remote / embedded replica) or a local libSQL/SQLite file. */\n// biome-ignore lint/suspicious/noExplicitAny: accepts every schema generic.\nexport type AnyLibSQLDatabase = LibSQLDatabase<any>;\n\n/** The libSQL SQL behind `createLibSQLSitepingStore`. */\nexport function createLibSQLGateway(\n db: AnyLibSQLDatabase,\n { sitepingFeedbacks, sitepingAnnotations, sitepingComments }: SitepingSqliteTables,\n): SitepingSqlGateway {\n const whereClause = (filter: FeedbackFilter) =>\n buildFeedbackWhere(sitepingFeedbacks, filter, CASE_INSENSITIVE_LIKE_OPERATOR.sqlite);\n const feedbackColumns = feedbackRecordColumns(getTableColumns(sitepingFeedbacks));\n const commentColumns = recordColumns(getTableColumns(sitepingComments));\n /** Number of feedback rows matching a where clause — the page `total`. */\n const countMatching = async (where: ReturnType<typeof whereClause>): Promise<number> => {\n const [totals] = await db.select({ total: count() }).from(sitepingFeedbacks).where(where);\n return totals?.total ?? 0;\n };\n // SQLite's implicit rowid is the insertion ordinal: the database assigns it on\n // every insert — the store's, or the host application's through the exported\n // table — under its single-writer lock, above every existing row's rowid\n // (without AUTOINCREMENT a deleted maximum may be reused, which still ranks\n // the new row after every row that remains).\n const insertionOrder = sql`${sitepingFeedbacks}.rowid`;\n\n // Multi-statement writes go through `db.batch`, never an interactive\n // `db.transaction`: libSQL runs a batch as one transaction without yielding\n // between its statements, so no write lock is held across an `await` and\n // the host application's own writes on the same database never hit\n // SQLITE_BUSY because of the store.\n return {\n async insertFeedback(feedback, annotations) {\n const insertFeedbackRow = db\n .insert(sitepingFeedbacks)\n .values(withSearchableMessage(feedback))\n .onConflictDoNothing({ target: sitepingFeedbacks.clientId })\n .returning({ id: sitepingFeedbacks.id });\n if (annotations.length === 0) return (await insertFeedbackRow).length > 0;\n\n // The feedback id is fresh, so it exists only when this batch inserted it.\n const [inserted] = await db.batch([\n insertFeedbackRow,\n db.insert(sitepingAnnotations).select(\n selectValues(\n sitepingAnnotations,\n annotations.map((annotation, position) => ({ ...annotation, position })),\n sql`EXISTS (SELECT 1 FROM ${sitepingFeedbacks} WHERE ${sitepingFeedbacks.id} = ${feedback.id})`,\n ),\n ),\n ]);\n return inserted.length > 0;\n },\n async findFeedbacks(filter, { limit, offset }) {\n const where = whereClause(filter);\n const [rows, total] = await Promise.all([\n db\n .select(feedbackColumns)\n .from(sitepingFeedbacks)\n .where(where)\n .orderBy(...newestFeedbackFirst(sitepingFeedbacks.createdAt, insertionOrder))\n .limit(limit)\n .offset(offset),\n countMatching(where),\n ]);\n return { rows, total };\n },\n async countFeedbacks(filter) {\n return countMatching(whereClause(filter));\n },\n async findAnnotations(feedbackIds) {\n return db\n .select(recordColumns(getTableColumns(sitepingAnnotations)))\n .from(sitepingAnnotations)\n .where(inArray(sitepingAnnotations.feedbackId, [...feedbackIds]))\n .orderBy(sitepingAnnotations.createdAt, sitepingAnnotations.position);\n },\n async findComments(feedbackIds) {\n return db\n .select(commentColumns)\n .from(sitepingComments)\n .where(inArray(sitepingComments.feedbackId, [...feedbackIds]))\n .orderBy(sitepingComments.createdAt, sitepingComments.position);\n },\n async findCommentByClientId(clientId) {\n const [row] = await db\n .select(commentColumns)\n .from(sitepingComments)\n .where(eq(sitepingComments.clientId, clientId))\n .limit(1);\n return row ?? null;\n },\n async insertComment(comment, maxComments) {\n const { values, condition } = guardedCommentInsert(\n { feedbacks: sitepingFeedbacks, comments: sitepingComments },\n comment,\n maxComments,\n );\n const inserted = await db\n .insert(sitepingComments)\n .select(selectValues(sitepingComments, [values], condition))\n .onConflictDoNothing({ target: sitepingComments.clientId })\n .returning({ id: sitepingComments.id });\n return inserted.length > 0;\n },\n async deleteComment(feedbackId, commentId) {\n const deleted = await db\n .delete(sitepingComments)\n .where(and(eq(sitepingComments.id, commentId), eq(sitepingComments.feedbackId, feedbackId)))\n .returning({ id: sitepingComments.id });\n return deleted.length > 0;\n },\n async findByClientId(clientId) {\n const [row] = await db\n .select(feedbackColumns)\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.clientId, clientId))\n .limit(1);\n return row ?? null;\n },\n async findProjectName(id) {\n const [row] = await db\n .select({ projectName: sitepingFeedbacks.projectName })\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.id, id))\n .limit(1);\n return row?.projectName ?? null;\n },\n async updateStatus(id, { status, resolvedAt, updatedAt }) {\n const [row] = await db\n .update(sitepingFeedbacks)\n .set({\n status,\n resolvedAt,\n updatedAt: monotonicUpdatedAt(sitepingFeedbacks, updatedAt, GREATEST_VALUE_FUNCTION.sqlite),\n })\n .where(eq(sitepingFeedbacks.id, id))\n .returning(feedbackColumns);\n return row ?? null;\n },\n async deleteById(id, options) {\n // Annotations and comments cascade only with `PRAGMA foreign_keys = ON`,\n // which libSQL does not guarantee — delete them explicitly in the same batch.\n const [, , deleted] = await db.batch([\n db.delete(sitepingAnnotations).where(eq(sitepingAnnotations.feedbackId, id)),\n db.delete(sitepingComments).where(eq(sitepingComments.feedbackId, id)),\n db\n .delete(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.id, id))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, options)),\n ]);\n return deleted.length > 0 ? toDeletedFeedbacks(deleted) : null;\n },\n async deleteByProject(projectName) {\n const projectFeedbackIds = db\n .select({ id: sitepingFeedbacks.id })\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.projectName, projectName));\n await db.batch([\n db.delete(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, projectFeedbackIds)),\n db.delete(sitepingComments).where(inArray(sitepingComments.feedbackId, projectFeedbackIds)),\n db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName)),\n ]);\n },\n async deleteProjectChunk(projectName, chunkSize) {\n // Ordered by the primary key, every statement of the batch picks the same\n // rows: nothing else writes the feedback table between them.\n const chunkIds = db\n .select({ id: sitepingFeedbacks.id })\n .from(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.projectName, projectName))\n .orderBy(sitepingFeedbacks.id)\n .limit(chunkSize);\n const [, , deleted] = await db.batch([\n db.delete(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, chunkIds)),\n db.delete(sitepingComments).where(inArray(sitepingComments.feedbackId, chunkIds)),\n db\n .delete(sitepingFeedbacks)\n .where(inArray(sitepingFeedbacks.id, chunkIds))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, { collectScreenshotUrls: true })),\n ]);\n return toDeletedFeedbacks(deleted);\n },\n async findReferencedScreenshotUrls(screenshotUrls) {\n const rows = await db\n .selectDistinct({ screenshotUrl: sitepingFeedbacks.screenshotUrl })\n .from(sitepingFeedbacks)\n .where(inArray(sitepingFeedbacks.screenshotUrl, [...screenshotUrls]));\n return new Set(rows.flatMap((row) => (row.screenshotUrl === null ? [] : [row.screenshotUrl])));\n },\n };\n}\n","import type {\n CommentAuthorRole,\n DiagnosticsSnapshot,\n FeedbackStatus,\n FeedbackType,\n ScreenshotRegion,\n} from \"@siteping/core\";\nimport { index, integer, real, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\nimport { DEFAULT_SITEPING_TABLE_NAMES, type SitepingTableNames } from \"../constants/table-names.js\";\n\n/**\n * Build the SitePing tables for SQLite / libSQL (Turso). Export them from\n * your Drizzle schema so `drizzle-kit generate` / `push` creates them:\n *\n * ```ts\n * // db/schema.ts\n * export const { sitepingFeedbacks, sitepingAnnotations, sitepingComments } = createSitepingSqliteTables();\n * ```\n *\n * JSON columns are stored as text and timestamps as epoch milliseconds. The\n * feedback table is an ordinary rowid table: its implicit `rowid`, which the\n * database assigns on every insert, breaks `createdAt` ties in the \"newest\n * first\" ordering — never redeclare it `WITHOUT ROWID`.\n */\nexport function createSitepingSqliteTables(names: SitepingTableNames = DEFAULT_SITEPING_TABLE_NAMES) {\n const sitepingFeedbacks = sqliteTable(\n names.feedbacks,\n {\n id: text(\"id\").primaryKey(),\n projectName: text(\"project_name\").notNull(),\n type: text(\"type\").$type<FeedbackType>().notNull(),\n message: text(\"message\").notNull(),\n status: text(\"status\").$type<FeedbackStatus>().notNull().default(\"open\"),\n url: text(\"url\").notNull(),\n urlPattern: text(\"url_pattern\"),\n screenshotUrl: text(\"screenshot_url\"),\n screenshotRegion: text(\"screenshot_region\", { mode: \"json\" }).$type<ScreenshotRegion>(),\n diagnostics: text(\"diagnostics\", { mode: \"json\" }).$type<DiagnosticsSnapshot>(),\n viewport: text(\"viewport\").notNull(),\n userAgent: text(\"user_agent\").notNull(),\n authorName: text(\"author_name\").notNull(),\n authorEmail: text(\"author_email\").notNull(),\n clientId: text(\"client_id\").notNull(),\n resolvedAt: integer(\"resolved_at\", { mode: \"timestamp_ms\" }),\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" }).notNull(),\n updatedAt: integer(\"updated_at\", { mode: \"timestamp_ms\" }).notNull(),\n // `message` lowercased in JavaScript (`String.prototype.toLowerCase`, Unicode-aware), the\n // column the text search reads: SQLite's LIKE folds only ASCII case, so `Échec` would not\n // match `échec`. The store fills it on insert (a feedback's message never changes); rows\n // written by the host application stay NULL and the search falls back to `message` with\n // ASCII-only folding. Internal — never part of the feedback record.\n messageSearch: text(\"message_search\"),\n },\n (table) => [\n uniqueIndex(`${names.feedbacks}_client_id_key`).on(table.clientId),\n index(`${names.feedbacks}_project_status_created_idx`).on(table.projectName, table.status, table.createdAt),\n index(`${names.feedbacks}_project_url_idx`).on(table.projectName, table.url),\n ],\n );\n\n const sitepingAnnotations = sqliteTable(\n names.annotations,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\")\n .notNull()\n .references(() => sitepingFeedbacks.id, { onDelete: \"cascade\" }),\n cssSelector: text(\"css_selector\").notNull(),\n xpath: text(\"xpath\").notNull(),\n textSnippet: text(\"text_snippet\").notNull(),\n elementTag: text(\"element_tag\").notNull(),\n elementId: text(\"element_id\"),\n textPrefix: text(\"text_prefix\").notNull(),\n textSuffix: text(\"text_suffix\").notNull(),\n fingerprint: text(\"fingerprint\").notNull(),\n neighborText: text(\"neighbor_text\").notNull(),\n anchorKey: text(\"anchor_key\"),\n xPct: real(\"x_pct\").notNull(),\n yPct: real(\"y_pct\").notNull(),\n wPct: real(\"w_pct\").notNull(),\n hPct: real(\"h_pct\").notNull(),\n scrollX: real(\"scroll_x\").notNull(),\n scrollY: real(\"scroll_y\").notNull(),\n viewportW: integer(\"viewport_w\").notNull(),\n viewportH: integer(\"viewport_h\").notNull(),\n devicePixelRatio: real(\"device_pixel_ratio\").notNull().default(1),\n // Submission index within the feedback: `annotations[0]` is the primary anchor, and\n // every annotation of a feedback shares one `createdAt`. Rows the host application\n // inserts without it default to 0 and keep their `createdAt` order.\n position: integer(\"position\").notNull().default(0),\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" }).notNull(),\n },\n (table) => [index(`${names.annotations}_feedback_id_idx`).on(table.feedbackId)],\n );\n\n const sitepingComments = sqliteTable(\n names.comments,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\")\n .notNull()\n .references(() => sitepingFeedbacks.id, { onDelete: \"cascade\" }),\n body: text(\"body\").notNull(),\n authorName: text(\"author_name\").notNull(),\n authorEmail: text(\"author_email\").notNull(),\n authorRole: text(\"author_role\").$type<CommentAuthorRole>().notNull().default(\"client\"),\n clientId: text(\"client_id\").notNull(),\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" }).notNull(),\n // Posting index within the thread, assigned by the insert itself: breaks `createdAt`\n // ties so a thread reads in posting order. Rows the host application inserts without\n // it default to 0 and keep their `createdAt` order.\n position: integer(\"position\").notNull().default(0),\n },\n (table) => [\n uniqueIndex(`${names.comments}_client_id_key`).on(table.clientId),\n index(`${names.comments}_feedback_created_idx`).on(table.feedbackId, table.createdAt),\n ],\n );\n\n return { sitepingFeedbacks, sitepingAnnotations, sitepingComments };\n}\n\nexport type SitepingSqliteTables = ReturnType<typeof createSitepingSqliteTables>;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where SitePing reports what goes wrong. The arguments come in console's and
3
+ * winston's order, so `console` and winston satisfy it as they are. pino's
4
+ * methods take the context first and type-check here anyway, then drop the
5
+ * context: adapt them — `{ error: (message, { error, ...context }) =>
6
+ * log.error({ err: error, ...context }, message), warn: … }`.
7
+ *
8
+ * Each option asks only for the level it reports — the handler's `logger`
9
+ * for `error`, the stores' and screenshot storages' for `warn`
10
+ * ({@link SitepingWarnLogger}) — so one object can be handed to all of them.
11
+ */
12
+ export interface SitepingLogger {
13
+ /** Unexpected failures: a request answered 500, a hook or a lookup failed. */
14
+ error(message: string, context: Record<string, unknown>): void | Promise<void>;
15
+ /**
16
+ * Degraded but handled: a screenshot dropped or left orphaned, screenshots
17
+ * stored inline. The handler never calls it; the stores and screenshot
18
+ * storages require it.
19
+ */
20
+ warn?(message: string, context: Record<string, unknown>): void | Promise<void>;
21
+ }
22
+ /** What the stores and screenshot storages report through: a {@link SitepingLogger} with `warn`. */
23
+ export type SitepingWarnLogger = Required<Pick<SitepingLogger, "warn">>;
24
+ /**
25
+ * Report through `logger.warn` without letting the logger fail its caller,
26
+ * on paths documented as non-throwing: a throw, or a rejection of what it
27
+ * returns — unhandled, fatal in Node by default — is dropped. A broken
28
+ * logger has no other channel to report through.
29
+ */
30
+ export declare function warnSafely(logger: SitepingWarnLogger, message: string, context: Record<string, unknown>): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where SitePing reports what goes wrong. The arguments come in console's and
3
+ * winston's order, so `console` and winston satisfy it as they are. pino's
4
+ * methods take the context first and type-check here anyway, then drop the
5
+ * context: adapt them — `{ error: (message, { error, ...context }) =>
6
+ * log.error({ err: error, ...context }, message), warn: … }`.
7
+ *
8
+ * Each option asks only for the level it reports — the handler's `logger`
9
+ * for `error`, the stores' and screenshot storages' for `warn`
10
+ * ({@link SitepingWarnLogger}) — so one object can be handed to all of them.
11
+ */
12
+ export interface SitepingLogger {
13
+ /** Unexpected failures: a request answered 500, a hook or a lookup failed. */
14
+ error(message: string, context: Record<string, unknown>): void | Promise<void>;
15
+ /**
16
+ * Degraded but handled: a screenshot dropped or left orphaned, screenshots
17
+ * stored inline. The handler never calls it; the stores and screenshot
18
+ * storages require it.
19
+ */
20
+ warn?(message: string, context: Record<string, unknown>): void | Promise<void>;
21
+ }
22
+ /** What the stores and screenshot storages report through: a {@link SitepingLogger} with `warn`. */
23
+ export type SitepingWarnLogger = Required<Pick<SitepingLogger, "warn">>;
24
+ /**
25
+ * Report through `logger.warn` without letting the logger fail its caller,
26
+ * on paths documented as non-throwing: a throw, or a rejection of what it
27
+ * returns — unhandled, fatal in Node by default — is dropped. A broken
28
+ * logger has no other channel to report through.
29
+ */
30
+ export declare function warnSafely(logger: SitepingWarnLogger, message: string, context: Record<string, unknown>): void;
package/dist/pg.cjs CHANGED
@@ -25,9 +25,14 @@ __export(pg_exports, {
25
25
  StoreLimitError: () => StoreLimitError,
26
26
  StoreNotFoundError: () => StoreNotFoundError,
27
27
  StorePersistenceError: () => StorePersistenceError,
28
+ StoreValueTooLongError: () => StoreValueTooLongError,
28
29
  createPgSitepingStore: () => createPgSitepingStore,
29
30
  createSitepingPgTables: () => createSitepingPgTables,
30
- isStorePersistence: () => isStorePersistence
31
+ isStoreDuplicate: () => isStoreDuplicate,
32
+ isStoreLimit: () => isStoreLimit,
33
+ isStoreNotFound: () => isStoreNotFound,
34
+ isStorePersistence: () => isStorePersistence,
35
+ isStoreValueTooLong: () => isStoreValueTooLong
31
36
  });
32
37
  module.exports = __toCommonJS(pg_exports);
33
38
 
@@ -56,6 +61,14 @@ async function settleWithConcurrencyLimit(items, concurrency, task) {
56
61
  return results;
57
62
  }
58
63
 
64
+ // ../core/src/type-utils.ts
65
+ function isRecord(value) {
66
+ return typeof value === "object" && value !== null;
67
+ }
68
+ function hasOwn(value, key) {
69
+ return isRecord(value) && key in value;
70
+ }
71
+
59
72
  // ../core/src/filters.ts
60
73
  var DEFAULT_PAGE_LIMIT = 50;
61
74
  var MAX_PAGE_LIMIT = 100;
@@ -71,12 +84,13 @@ function isUnreachableOffset(skip) {
71
84
  return !Number.isSafeInteger(skip);
72
85
  }
73
86
 
74
- // ../core/src/type-utils.ts
75
- function isRecord(value) {
76
- return typeof value === "object" && value !== null;
77
- }
78
- function hasOwn(value, key) {
79
- return isRecord(value) && key in value;
87
+ // ../core/src/logger.ts
88
+ function warnSafely(logger, message, context) {
89
+ try {
90
+ Promise.resolve(logger.warn(message, context)).catch(() => {
91
+ });
92
+ } catch {
93
+ }
80
94
  }
81
95
 
82
96
  // ../core/src/types.ts
@@ -109,6 +123,13 @@ var StoreLimitError = class extends Error {
109
123
  this.name = "StoreLimitError";
110
124
  }
111
125
  };
126
+ var StoreValueTooLongError = class extends Error {
127
+ code = "STORE_VALUE_TOO_LONG";
128
+ constructor(message = "Value too long for the store", options) {
129
+ super(message, options);
130
+ this.name = "StoreValueTooLongError";
131
+ }
132
+ };
112
133
  function hasErrorCode(error, code) {
113
134
  return hasOwn(error, "code") && error.code === code;
114
135
  }
@@ -124,6 +145,14 @@ function isStorePersistence(error) {
124
145
  if (error instanceof StorePersistenceError) return true;
125
146
  return hasErrorCode(error, "STORE_PERSISTENCE");
126
147
  }
148
+ function isStoreLimit(error) {
149
+ if (error instanceof StoreLimitError) return true;
150
+ return hasErrorCode(error, "STORE_LIMIT");
151
+ }
152
+ function isStoreValueTooLong(error) {
153
+ if (error instanceof StoreValueTooLongError) return true;
154
+ return hasErrorCode(error, "STORE_VALUE_TOO_LONG");
155
+ }
127
156
 
128
157
  // ../core/src/screenshot-storage.ts
129
158
  var SCREENSHOT_DELETE_CONCURRENCY = 32;
@@ -197,6 +226,26 @@ function buildFeedbackRecord(input, ctx) {
197
226
  };
198
227
  }
199
228
 
229
+ // ../core/src/text.ts
230
+ var UNSTORABLE_CODE_UNITS = /\0|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
231
+ var REPLACEMENT_CHARACTER = "\uFFFD";
232
+ function toStorableText(text2) {
233
+ return text2.replace(UNSTORABLE_CODE_UNITS, REPLACEMENT_CHARACTER);
234
+ }
235
+ function isPlainObject(value) {
236
+ if (typeof value !== "object" || value === null) return false;
237
+ const prototype = Object.getPrototypeOf(value);
238
+ return prototype === Object.prototype || prototype === null;
239
+ }
240
+ function toStorableValue(value) {
241
+ if (typeof value === "string") return toStorableText(value);
242
+ if (Array.isArray(value)) return value.map(toStorableValue);
243
+ if (!isPlainObject(value)) return value;
244
+ return Object.fromEntries(
245
+ Object.entries(value).map(([key, item]) => [toStorableText(key), toStorableValue(item)])
246
+ );
247
+ }
248
+
200
249
  // src/constants/deletes.ts
201
250
  var PROJECT_DELETE_CHUNK_SIZE = 500;
202
251
 
@@ -262,28 +311,6 @@ function withDriverErrors(gateway) {
262
311
  });
263
312
  }
264
313
 
265
- // src/constants/text.ts
266
- var UNSTORABLE_CODE_UNITS = /\0|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
267
- var REPLACEMENT_CHARACTER = "\uFFFD";
268
-
269
- // src/shared/text.ts
270
- function toStorableText(text2) {
271
- return text2.replace(UNSTORABLE_CODE_UNITS, REPLACEMENT_CHARACTER);
272
- }
273
- function isPlainObject(value) {
274
- if (typeof value !== "object" || value === null) return false;
275
- const prototype = Object.getPrototypeOf(value);
276
- return prototype === Object.prototype || prototype === null;
277
- }
278
- function toStorableValue(value) {
279
- if (typeof value === "string") return toStorableText(value);
280
- if (Array.isArray(value)) return value.map(toStorableValue);
281
- if (!isPlainObject(value)) return value;
282
- return Object.fromEntries(
283
- Object.entries(value).map(([key, item]) => [toStorableText(key), toStorableValue(item)])
284
- );
285
- }
286
-
287
314
  // src/shared/store.ts
288
315
  var silentLogger = {
289
316
  warn() {
@@ -336,8 +363,11 @@ var DrizzleSitepingStore = class {
336
363
  this.logger = options.logger ?? silentLogger;
337
364
  this.now = options.now ?? systemClock;
338
365
  }
366
+ /** Throws `StoreDuplicateError` when a feedback with the same `clientId` exists. */
339
367
  async createFeedback(data) {
340
- return (await this.createFeedbackIfAbsent(data)).feedback;
368
+ const { feedback, created } = await this.createFeedbackIfAbsent(data);
369
+ if (!created) throw new StoreDuplicateError(`A feedback with clientId ${data.clientId} already exists`);
370
+ return feedback;
341
371
  }
342
372
  /**
343
373
  * Insert the feedback unless a row with the same `clientId` exists. The
@@ -519,7 +549,15 @@ var DrizzleSitepingStore = class {
519
549
  * @throws `StoreLimitError` when a `client` comment meets a thread already holding `MAX_COMMENTS_PER_FEEDBACK` of them.
520
550
  * @throws `StorePersistenceError` when a database call fails.
521
551
  */
522
- async addComment(submittedFeedbackId, submitted) {
552
+ async addComment(feedbackId, data) {
553
+ return (await this.addCommentIfAbsent(feedbackId, data)).comment;
554
+ }
555
+ /**
556
+ * {@link addComment}, reporting whether this call inserted the comment: the
557
+ * single statement inserts at most one row per `clientId`, so of racing
558
+ * posts exactly one reports `created: true`.
559
+ */
560
+ async addCommentIfAbsent(submittedFeedbackId, submitted) {
523
561
  const feedbackId = toStorableText(submittedFeedbackId);
524
562
  const data = toStorableValue(submitted);
525
563
  const comment = buildCommentRecord(data, { id: crypto.randomUUID(), feedbackId, now: this.now() });
@@ -540,13 +578,13 @@ var DrizzleSitepingStore = class {
540
578
  if (feedbackGone) throw new StoreNotFoundError();
541
579
  throw error;
542
580
  }
543
- if (inserted) return comment;
581
+ if (inserted) return { comment, created: true };
544
582
  const replayed = await persistMutation(
545
583
  "addComment",
546
584
  identifiers,
547
585
  () => this.gateway.findCommentByClientId(data.clientId)
548
586
  );
549
- if (replayed) return replayed;
587
+ if (replayed) return { comment: replayed, created: false };
550
588
  const projectName = await persistMutation(
551
589
  "addComment",
552
590
  identifiers,
@@ -613,7 +651,8 @@ var DrizzleSitepingStore = class {
613
651
  });
614
652
  return url;
615
653
  } catch (error) {
616
- this.logger.warn(
654
+ warnSafely(
655
+ this.logger,
617
656
  "[siteping] DrizzleStore: screenshotStorage.upload failed \u2014 feedback saved without screenshot",
618
657
  { clientId, feedbackId, error }
619
658
  );
@@ -622,7 +661,8 @@ var DrizzleSitepingStore = class {
622
661
  }
623
662
  if (!this.inlineScreenshotWarned) {
624
663
  this.inlineScreenshotWarned = true;
625
- this.logger.warn(
664
+ warnSafely(
665
+ this.logger,
626
666
  "[siteping] DrizzleStore: no screenshotStorage configured \u2014 screenshots are stored inline as base64. Configure a ScreenshotStorage for production.",
627
667
  {}
628
668
  );
@@ -665,7 +705,8 @@ var DrizzleSitepingStore = class {
665
705
  try {
666
706
  referenced = await this.findReferencedScreenshotUrls(candidates);
667
707
  } catch (lookupError) {
668
- this.logger.warn(
708
+ warnSafely(
709
+ this.logger,
669
710
  `${DRIZZLE_STORE_MESSAGE_PREFIX}: screenshot references could not be checked \u2014 screenshots kept`,
670
711
  { ...context, screenshotUrls: candidates, error: lookupError }
671
712
  );
@@ -679,11 +720,15 @@ var DrizzleSitepingStore = class {
679
720
  );
680
721
  results.forEach((result, index2) => {
681
722
  if (result.status === "rejected") {
682
- this.logger.warn(`${DRIZZLE_STORE_MESSAGE_PREFIX}: screenshotStorage.delete failed \u2014 object left in place`, {
683
- ...context,
684
- screenshotUrl: unreferenced[index2],
685
- error: result.reason
686
- });
723
+ warnSafely(
724
+ this.logger,
725
+ `${DRIZZLE_STORE_MESSAGE_PREFIX}: screenshotStorage.delete failed \u2014 object left in place`,
726
+ {
727
+ ...context,
728
+ screenshotUrl: unreferenced[index2],
729
+ error: result.reason
730
+ }
731
+ );
687
732
  }
688
733
  });
689
734
  }