@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/pg.d.cts CHANGED
@@ -3,7 +3,7 @@ export { DEFAULT_SITEPING_TABLE_NAMES, DrizzleStoreLogger } from './index.cjs';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
4
  import { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-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 PostgreSQL. Export them from your Drizzle
@@ -965,4 +965,5 @@ interface PgSitepingStoreOptions extends DrizzleStoreOptions {
965
965
  */
966
966
  declare function createPgSitepingStore(db: AnyPgDatabase, options?: PgSitepingStoreOptions): DrizzleStore;
967
967
 
968
- export { type AnyPgDatabase, DrizzleStore, DrizzleStoreOptions, type PgSitepingStoreOptions, type SitepingPgTables, SitepingTableNames, createPgSitepingStore, createSitepingPgTables };
968
+ export { DrizzleStore, DrizzleStoreOptions, SitepingTableNames, createPgSitepingStore, createSitepingPgTables };
969
+ export type { AnyPgDatabase, PgSitepingStoreOptions, SitepingPgTables };
package/dist/pg.d.ts CHANGED
@@ -3,7 +3,7 @@ export { DEFAULT_SITEPING_TABLE_NAMES, DrizzleStoreLogger } from './index.js';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
4
  import { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-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 PostgreSQL. Export them from your Drizzle
@@ -965,4 +965,5 @@ interface PgSitepingStoreOptions extends DrizzleStoreOptions {
965
965
  */
966
966
  declare function createPgSitepingStore(db: AnyPgDatabase, options?: PgSitepingStoreOptions): DrizzleStore;
967
967
 
968
- export { type AnyPgDatabase, DrizzleStore, DrizzleStoreOptions, type PgSitepingStoreOptions, type SitepingPgTables, SitepingTableNames, createPgSitepingStore, createSitepingPgTables };
968
+ export { DrizzleStore, DrizzleStoreOptions, SitepingTableNames, createPgSitepingStore, createSitepingPgTables };
969
+ export type { AnyPgDatabase, PgSitepingStoreOptions, SitepingPgTables };
package/dist/pg.js CHANGED
@@ -10,11 +10,16 @@ import {
10
10
  StoreLimitError,
11
11
  StoreNotFoundError,
12
12
  StorePersistenceError,
13
+ StoreValueTooLongError,
13
14
  buildFeedbackWhere,
14
15
  deletedFeedbackColumns,
15
16
  feedbackRecordColumns,
16
17
  guardedCommentInsert,
18
+ isStoreDuplicate,
19
+ isStoreLimit,
20
+ isStoreNotFound,
17
21
  isStorePersistence,
22
+ isStoreValueTooLong,
18
23
  monotonicUpdatedAt,
19
24
  newestFeedbackFirst,
20
25
  primaryOf,
@@ -22,7 +27,7 @@ import {
22
27
  selectValues,
23
28
  toDeletedFeedbacks,
24
29
  withSearchableMessage
25
- } from "./chunk-6G6UAERF.js";
30
+ } from "./chunk-6DO75JHR.js";
26
31
  import {
27
32
  DEFAULT_SITEPING_TABLE_NAMES
28
33
  } from "./chunk-ICY2AW6S.js";
@@ -291,8 +296,13 @@ export {
291
296
  StoreLimitError,
292
297
  StoreNotFoundError,
293
298
  StorePersistenceError,
299
+ StoreValueTooLongError,
294
300
  createPgSitepingStore,
295
301
  createSitepingPgTables,
296
- isStorePersistence
302
+ isStoreDuplicate,
303
+ isStoreLimit,
304
+ isStoreNotFound,
305
+ isStorePersistence,
306
+ isStoreValueTooLong
297
307
  };
298
308
  //# sourceMappingURL=pg.js.map
package/dist/pg.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pg/gateway.ts","../src/pg/tables.ts","../src/pg/index.ts"],"sourcesContent":["import { and, type Column, count, eq, getTableColumns, inArray, type SQL, sql, type WithSubquery } from \"drizzle-orm\";\nimport type { PgDatabase, PgQueryResultHKT } from \"drizzle-orm/pg-core\";\nimport { CASE_INSENSITIVE_LIKE_OPERATOR } from \"../constants/search.js\";\nimport {\n GREATEST_VALUE_FUNCTION,\n INSERTED_ANNOTATIONS_CTE_ALIAS,\n INSERTED_FEEDBACK_CTE_ALIAS,\n} 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 { SitepingPgTables } from \"./tables.js\";\n\n/**\n * Any Drizzle PostgreSQL database — node-postgres, postgres.js, Neon\n * (serverless / HTTP), Vercel Postgres, Supabase, PGlite… The store never\n * opens an interactive `db.transaction`: every write is a single statement,\n * so drivers without interactive transactions (Neon HTTP) work too.\n */\n// biome-ignore lint/suspicious/noExplicitAny: accepts every driver's query-result HKT and schema.\nexport type AnyPgDatabase = PgDatabase<PgQueryResultHKT, any, any>;\n\n/**\n * Type a bound parameter as its target column: parameters inside a `VALUES`\n * list carry no type of their own in PostgreSQL.\n */\nfunction castToColumnType(param: SQL, column: Column): SQL {\n return sql`CAST(${param} AS ${sql.raw(column.getSQLType())})`;\n}\n\n/** The PostgreSQL SQL behind `createPgSitepingStore`. */\nexport function createPgGateway(\n db: AnyPgDatabase,\n { sitepingFeedbacks, sitepingAnnotations, sitepingComments }: SitepingPgTables,\n): SitepingSqlGateway {\n const whereClause = (filter: FeedbackFilter) =>\n buildFeedbackWhere(sitepingFeedbacks, filter, CASE_INSENSITIVE_LIKE_OPERATOR.postgres);\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\n return {\n /**\n * One statement, no interactive transaction (Neon HTTP has none): the\n * feedback insert is a CTE, and the annotation insert selects from it —\n * so annotations land only with the feedback row, atomically.\n */\n async insertFeedback(feedback, annotations) {\n const insertedFeedback = db\n .$with(INSERTED_FEEDBACK_CTE_ALIAS)\n .as(\n db\n .insert(sitepingFeedbacks)\n .values(withSearchableMessage(feedback))\n .onConflictDoNothing({ target: sitepingFeedbacks.clientId })\n .returning({ id: sitepingFeedbacks.id }),\n );\n const statements: WithSubquery[] = [insertedFeedback];\n if (annotations.length > 0) {\n // Data-modifying CTEs always run to completion, even unreferenced.\n statements.push(\n db.$with(INSERTED_ANNOTATIONS_CTE_ALIAS).as(\n db.insert(sitepingAnnotations).select(\n selectValues(\n sitepingAnnotations,\n annotations.map((annotation, position) => ({ ...annotation, position })),\n sql`EXISTS (SELECT 1 FROM ${insertedFeedback})`,\n castToColumnType,\n ),\n ),\n ),\n );\n }\n const inserted = await db\n .with(...statements)\n .select({ id: insertedFeedback.id })\n .from(insertedFeedback);\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, sitepingFeedbacks.creationSequence))\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, castToColumnType))\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.postgres),\n })\n .where(eq(sitepingFeedbacks.id, id))\n .returning(feedbackColumns);\n return row ?? null;\n },\n async deleteById(id, options) {\n const rows = await db\n .delete(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.id, id))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, options));\n return rows.length > 0 ? toDeletedFeedbacks(rows) : null;\n },\n // Annotations and comments follow their feedback through the `ON DELETE CASCADE` foreign keys.\n async deleteByProject(projectName) {\n await db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName));\n },\n async deleteProjectChunk(projectName, chunkSize) {\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 rows = await db\n .delete(sitepingFeedbacks)\n .where(inArray(sitepingFeedbacks.id, chunkIds))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, { collectScreenshotUrls: true }));\n return toDeletedFeedbacks(rows);\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 {\n bigint,\n doublePrecision,\n foreignKey,\n getTableConfig,\n index,\n integer,\n jsonb,\n type PgTable,\n pgTable,\n text,\n timestamp,\n uniqueIndex,\n} from \"drizzle-orm/pg-core\";\nimport { DRIZZLE_STORE_MESSAGE_PREFIX } from \"../constants/errors.js\";\nimport { POSTGRES_IDENTIFIER_MAX_BYTES } from \"../constants/sql.js\";\nimport { DEFAULT_SITEPING_TABLE_NAMES, type SitepingTableNames } from \"../constants/table-names.js\";\n\n/**\n * Refuse identifiers PostgreSQL would truncate. It cuts every name past\n * {@link POSTGRES_IDENTIFIER_MAX_BYTES} bytes without an error, so each later\n * `drizzle-kit push` or `generate` sees the declared name missing and drops\n * and recreates the index or foreign key — or two names cut to the same\n * prefix and the migration fails.\n *\n * @param tables - The tables, whose own, index and foreign-key names are checked.\n */\nfunction assertIdentifiersFitPostgres(tables: readonly PgTable[]): void {\n const encoder = new TextEncoder();\n for (const table of tables) {\n const { name, indexes, foreignKeys } = getTableConfig(table);\n const identifiers = [\n name,\n ...indexes.flatMap((tableIndex) => tableIndex.config.name ?? []),\n ...foreignKeys.map((key) => key.getName()),\n ];\n for (const identifier of identifiers) {\n const bytes = encoder.encode(identifier).length;\n if (bytes > POSTGRES_IDENTIFIER_MAX_BYTES) {\n throw new RangeError(\n `${DRIZZLE_STORE_MESSAGE_PREFIX}: \"${identifier}\" is ${bytes} bytes long, over PostgreSQL's ${POSTGRES_IDENTIFIER_MAX_BYTES}-byte identifier limit — pass shorter table names to createSitepingPgTables`,\n );\n }\n }\n }\n}\n\n/**\n * Build the SitePing tables for PostgreSQL. Export them from your Drizzle\n * schema so `drizzle-kit generate` / `push` creates them:\n *\n * ```ts\n * // db/schema.ts\n * export const { sitepingFeedbacks, sitepingAnnotations, sitepingComments } = createSitepingPgTables();\n * ```\n *\n * Index and foreign-key names derive from the table names, which leaves the\n * feedback table at most 36 bytes, annotations 47 and comments 42.\n *\n * @throws `RangeError` when a table, index or foreign-key name would pass\n * PostgreSQL's 63-byte identifier limit.\n */\nexport function createSitepingPgTables(names: SitepingTableNames = DEFAULT_SITEPING_TABLE_NAMES) {\n const sitepingFeedbacks = pgTable(\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: jsonb(\"screenshot_region\").$type<ScreenshotRegion>(),\n diagnostics: jsonb(\"diagnostics\").$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: timestamp(\"resolved_at\", { withTimezone: true, precision: 3 }),\n createdAt: timestamp(\"created_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\n // Database-wide insertion ordinal: breaks `createdAt` ties between rows created in the\n // same millisecond, by any store instance or process — or by the host application\n // inserting directly — so \"newest first\" and offset pages stay stable. The database\n // assigns it on every insert. Internal — never part of the feedback record.\n creationSequence: bigint(\"creation_sequence\", { mode: \"number\" }).generatedAlwaysAsIdentity(),\n // `message` lowercased in JavaScript (`String.prototype.toLowerCase`, Unicode-aware), the\n // column the text search reads: `ILIKE` folds case with the collation / `LC_CTYPE`, so under\n // `C` `Échec` would not match `échec`. The store fills it on insert (a feedback's message\n // never changes); rows written by the host application stay NULL and the search falls\n // back to `message ILIKE`. 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 = pgTable(\n names.annotations,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\").notNull(),\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: doublePrecision(\"x_pct\").notNull(),\n yPct: doublePrecision(\"y_pct\").notNull(),\n wPct: doublePrecision(\"w_pct\").notNull(),\n hPct: doublePrecision(\"h_pct\").notNull(),\n scrollX: doublePrecision(\"scroll_x\").notNull(),\n scrollY: doublePrecision(\"scroll_y\").notNull(),\n viewportW: integer(\"viewport_w\").notNull(),\n viewportH: integer(\"viewport_h\").notNull(),\n devicePixelRatio: doublePrecision(\"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: timestamp(\"created_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\n },\n (table) => [\n foreignKey({\n name: `${names.annotations}_feedback_id_fk`,\n columns: [table.feedbackId],\n foreignColumns: [sitepingFeedbacks.id],\n }).onDelete(\"cascade\"),\n index(`${names.annotations}_feedback_id_idx`).on(table.feedbackId),\n ],\n );\n\n const sitepingComments = pgTable(\n names.comments,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\").notNull(),\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: timestamp(\"created_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\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 foreignKey({\n name: `${names.comments}_feedback_id_fk`,\n columns: [table.feedbackId],\n foreignColumns: [sitepingFeedbacks.id],\n }).onDelete(\"cascade\"),\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 assertIdentifiersFitPostgres([sitepingFeedbacks, sitepingAnnotations, sitepingComments]);\n return { sitepingFeedbacks, sitepingAnnotations, sitepingComments };\n}\n\nexport type SitepingPgTables = ReturnType<typeof createSitepingPgTables>;\n","import { primaryOf } from \"../shared/replicas.js\";\nimport { DrizzleSitepingStore, type DrizzleStore, type DrizzleStoreOptions } from \"../shared/store.js\";\nimport { type AnyPgDatabase, createPgGateway } from \"./gateway.js\";\nimport { createSitepingPgTables, type SitepingPgTables } 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 { AnyPgDatabase } from \"./gateway.js\";\nexport { createSitepingPgTables, type SitepingPgTables } from \"./tables.js\";\n\nexport interface PgSitepingStoreOptions extends DrizzleStoreOptions {\n /** Tables built with `createSitepingPgTables` — pass them when you customized the names. */\n tables?: SitepingPgTables | undefined;\n}\n\n/**\n * `SitepingStore` on PostgreSQL through Drizzle ORM. Given a database built\n * with `withReplicas`, it runs everything on the primary.\n *\n * @example\n * ```ts\n * import { drizzle } from \"drizzle-orm/node-postgres\";\n * import { createPgSitepingStore } from \"@siteping/adapter-drizzle/pg\";\n *\n * const store = createPgSitepingStore(drizzle(process.env.DATABASE_URL!), { screenshotStorage });\n * ```\n */\nexport function createPgSitepingStore(db: AnyPgDatabase, options: PgSitepingStoreOptions = {}): DrizzleStore {\n const { tables = createSitepingPgTables(), ...storeOptions } = options;\n return new DrizzleSitepingStore(createPgGateway(primaryOf(db), tables), storeOptions);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/pg/gateway.ts","../src/pg/tables.ts","../src/pg/index.ts"],"sourcesContent":["import { and, type Column, count, eq, getTableColumns, inArray, type SQL, sql, type WithSubquery } from \"drizzle-orm\";\nimport type { PgDatabase, PgQueryResultHKT } from \"drizzle-orm/pg-core\";\nimport { CASE_INSENSITIVE_LIKE_OPERATOR } from \"../constants/search.js\";\nimport {\n GREATEST_VALUE_FUNCTION,\n INSERTED_ANNOTATIONS_CTE_ALIAS,\n INSERTED_FEEDBACK_CTE_ALIAS,\n} 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 { SitepingPgTables } from \"./tables.js\";\n\n/**\n * Any Drizzle PostgreSQL database — node-postgres, postgres.js, Neon\n * (serverless / HTTP), Vercel Postgres, Supabase, PGlite… The store never\n * opens an interactive `db.transaction`: every write is a single statement,\n * so drivers without interactive transactions (Neon HTTP) work too.\n */\n// biome-ignore lint/suspicious/noExplicitAny: accepts every driver's query-result HKT and schema.\nexport type AnyPgDatabase = PgDatabase<PgQueryResultHKT, any, any>;\n\n/**\n * Type a bound parameter as its target column: parameters inside a `VALUES`\n * list carry no type of their own in PostgreSQL.\n */\nfunction castToColumnType(param: SQL, column: Column): SQL {\n return sql`CAST(${param} AS ${sql.raw(column.getSQLType())})`;\n}\n\n/** The PostgreSQL SQL behind `createPgSitepingStore`. */\nexport function createPgGateway(\n db: AnyPgDatabase,\n { sitepingFeedbacks, sitepingAnnotations, sitepingComments }: SitepingPgTables,\n): SitepingSqlGateway {\n const whereClause = (filter: FeedbackFilter) =>\n buildFeedbackWhere(sitepingFeedbacks, filter, CASE_INSENSITIVE_LIKE_OPERATOR.postgres);\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\n return {\n /**\n * One statement, no interactive transaction (Neon HTTP has none): the\n * feedback insert is a CTE, and the annotation insert selects from it —\n * so annotations land only with the feedback row, atomically.\n */\n async insertFeedback(feedback, annotations) {\n const insertedFeedback = db\n .$with(INSERTED_FEEDBACK_CTE_ALIAS)\n .as(\n db\n .insert(sitepingFeedbacks)\n .values(withSearchableMessage(feedback))\n .onConflictDoNothing({ target: sitepingFeedbacks.clientId })\n .returning({ id: sitepingFeedbacks.id }),\n );\n const statements: WithSubquery[] = [insertedFeedback];\n if (annotations.length > 0) {\n // Data-modifying CTEs always run to completion, even unreferenced.\n statements.push(\n db.$with(INSERTED_ANNOTATIONS_CTE_ALIAS).as(\n db.insert(sitepingAnnotations).select(\n selectValues(\n sitepingAnnotations,\n annotations.map((annotation, position) => ({ ...annotation, position })),\n sql`EXISTS (SELECT 1 FROM ${insertedFeedback})`,\n castToColumnType,\n ),\n ),\n ),\n );\n }\n const inserted = await db\n .with(...statements)\n .select({ id: insertedFeedback.id })\n .from(insertedFeedback);\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, sitepingFeedbacks.creationSequence))\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, castToColumnType))\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.postgres),\n })\n .where(eq(sitepingFeedbacks.id, id))\n .returning(feedbackColumns);\n return row ?? null;\n },\n async deleteById(id, options) {\n const rows = await db\n .delete(sitepingFeedbacks)\n .where(eq(sitepingFeedbacks.id, id))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, options));\n return rows.length > 0 ? toDeletedFeedbacks(rows) : null;\n },\n // Annotations and comments follow their feedback through the `ON DELETE CASCADE` foreign keys.\n async deleteByProject(projectName) {\n await db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName));\n },\n async deleteProjectChunk(projectName, chunkSize) {\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 rows = await db\n .delete(sitepingFeedbacks)\n .where(inArray(sitepingFeedbacks.id, chunkIds))\n .returning(deletedFeedbackColumns(sitepingFeedbacks, { collectScreenshotUrls: true }));\n return toDeletedFeedbacks(rows);\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 {\n bigint,\n doublePrecision,\n foreignKey,\n getTableConfig,\n index,\n integer,\n jsonb,\n type PgTable,\n pgTable,\n text,\n timestamp,\n uniqueIndex,\n} from \"drizzle-orm/pg-core\";\nimport { DRIZZLE_STORE_MESSAGE_PREFIX } from \"../constants/errors.js\";\nimport { POSTGRES_IDENTIFIER_MAX_BYTES } from \"../constants/sql.js\";\nimport { DEFAULT_SITEPING_TABLE_NAMES, type SitepingTableNames } from \"../constants/table-names.js\";\n\n/**\n * Refuse identifiers PostgreSQL would truncate. It cuts every name past\n * {@link POSTGRES_IDENTIFIER_MAX_BYTES} bytes without an error, so each later\n * `drizzle-kit push` or `generate` sees the declared name missing and drops\n * and recreates the index or foreign key — or two names cut to the same\n * prefix and the migration fails.\n *\n * @param tables - The tables, whose own, index and foreign-key names are checked.\n */\nfunction assertIdentifiersFitPostgres(tables: readonly PgTable[]): void {\n const encoder = new TextEncoder();\n for (const table of tables) {\n const { name, indexes, foreignKeys } = getTableConfig(table);\n const identifiers = [\n name,\n ...indexes.flatMap((tableIndex) => tableIndex.config.name ?? []),\n ...foreignKeys.map((key) => key.getName()),\n ];\n for (const identifier of identifiers) {\n const bytes = encoder.encode(identifier).length;\n if (bytes > POSTGRES_IDENTIFIER_MAX_BYTES) {\n throw new RangeError(\n `${DRIZZLE_STORE_MESSAGE_PREFIX}: \"${identifier}\" is ${bytes} bytes long, over PostgreSQL's ${POSTGRES_IDENTIFIER_MAX_BYTES}-byte identifier limit — pass shorter table names to createSitepingPgTables`,\n );\n }\n }\n }\n}\n\n/**\n * Build the SitePing tables for PostgreSQL. Export them from your Drizzle\n * schema so `drizzle-kit generate` / `push` creates them:\n *\n * ```ts\n * // db/schema.ts\n * export const { sitepingFeedbacks, sitepingAnnotations, sitepingComments } = createSitepingPgTables();\n * ```\n *\n * Index and foreign-key names derive from the table names, which leaves the\n * feedback table at most 36 bytes, annotations 47 and comments 42.\n *\n * @throws `RangeError` when a table, index or foreign-key name would pass\n * PostgreSQL's 63-byte identifier limit.\n */\nexport function createSitepingPgTables(names: SitepingTableNames = DEFAULT_SITEPING_TABLE_NAMES) {\n const sitepingFeedbacks = pgTable(\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: jsonb(\"screenshot_region\").$type<ScreenshotRegion>(),\n diagnostics: jsonb(\"diagnostics\").$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: timestamp(\"resolved_at\", { withTimezone: true, precision: 3 }),\n createdAt: timestamp(\"created_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\n // Database-wide insertion ordinal: breaks `createdAt` ties between rows created in the\n // same millisecond, by any store instance or process — or by the host application\n // inserting directly — so \"newest first\" and offset pages stay stable. The database\n // assigns it on every insert. Internal — never part of the feedback record.\n creationSequence: bigint(\"creation_sequence\", { mode: \"number\" }).generatedAlwaysAsIdentity(),\n // `message` lowercased in JavaScript (`String.prototype.toLowerCase`, Unicode-aware), the\n // column the text search reads: `ILIKE` folds case with the collation / `LC_CTYPE`, so under\n // `C` `Échec` would not match `échec`. The store fills it on insert (a feedback's message\n // never changes); rows written by the host application stay NULL and the search falls\n // back to `message ILIKE`. 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 = pgTable(\n names.annotations,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\").notNull(),\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: doublePrecision(\"x_pct\").notNull(),\n yPct: doublePrecision(\"y_pct\").notNull(),\n wPct: doublePrecision(\"w_pct\").notNull(),\n hPct: doublePrecision(\"h_pct\").notNull(),\n scrollX: doublePrecision(\"scroll_x\").notNull(),\n scrollY: doublePrecision(\"scroll_y\").notNull(),\n viewportW: integer(\"viewport_w\").notNull(),\n viewportH: integer(\"viewport_h\").notNull(),\n devicePixelRatio: doublePrecision(\"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: timestamp(\"created_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\n },\n (table) => [\n foreignKey({\n name: `${names.annotations}_feedback_id_fk`,\n columns: [table.feedbackId],\n foreignColumns: [sitepingFeedbacks.id],\n }).onDelete(\"cascade\"),\n index(`${names.annotations}_feedback_id_idx`).on(table.feedbackId),\n ],\n );\n\n const sitepingComments = pgTable(\n names.comments,\n {\n id: text(\"id\").primaryKey(),\n feedbackId: text(\"feedback_id\").notNull(),\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: timestamp(\"created_at\", { withTimezone: true, precision: 3 }).notNull().defaultNow(),\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 foreignKey({\n name: `${names.comments}_feedback_id_fk`,\n columns: [table.feedbackId],\n foreignColumns: [sitepingFeedbacks.id],\n }).onDelete(\"cascade\"),\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 assertIdentifiersFitPostgres([sitepingFeedbacks, sitepingAnnotations, sitepingComments]);\n return { sitepingFeedbacks, sitepingAnnotations, sitepingComments };\n}\n\nexport type SitepingPgTables = ReturnType<typeof createSitepingPgTables>;\n","import { primaryOf } from \"../shared/replicas.js\";\nimport { DrizzleSitepingStore, type DrizzleStore, type DrizzleStoreOptions } from \"../shared/store.js\";\nimport { type AnyPgDatabase, createPgGateway } from \"./gateway.js\";\nimport { createSitepingPgTables, type SitepingPgTables } 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 { AnyPgDatabase } from \"./gateway.js\";\nexport { createSitepingPgTables, type SitepingPgTables } from \"./tables.js\";\n\nexport interface PgSitepingStoreOptions extends DrizzleStoreOptions {\n /** Tables built with `createSitepingPgTables` — pass them when you customized the names. */\n tables?: SitepingPgTables | undefined;\n}\n\n/**\n * `SitepingStore` on PostgreSQL through Drizzle ORM. Given a database built\n * with `withReplicas`, it runs everything on the primary.\n *\n * @example\n * ```ts\n * import { drizzle } from \"drizzle-orm/node-postgres\";\n * import { createPgSitepingStore } from \"@siteping/adapter-drizzle/pg\";\n *\n * const store = createPgSitepingStore(drizzle(process.env.DATABASE_URL!), { screenshotStorage });\n * ```\n */\nexport function createPgSitepingStore(db: AnyPgDatabase, options: PgSitepingStoreOptions = {}): DrizzleStore {\n const { tables = createSitepingPgTables(), ...storeOptions } = options;\n return new DrizzleSitepingStore(createPgGateway(primaryOf(db), tables), storeOptions);\n}\n"],"mappings":"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","names":[]}
package/dist/schema.d.cts CHANGED
@@ -52,6 +52,13 @@ export interface FieldDef {
52
52
  isUnique?: boolean;
53
53
  /** Prisma native type attribute (e.g. "Text" for @db.Text) — used for MySQL compatibility on long strings */
54
54
  nativeType?: PrismaNativeType;
55
+ /**
56
+ * Longest value the HTTP handler accepts for this column, in characters —
57
+ * set on every `String` column filled from a request body, and checked
58
+ * against the handler's own caps. The CLI sizes the column from it where a
59
+ * plain `String` is shorter: on MySQL, a `VARCHAR(191)`.
60
+ */
61
+ maxLength?: number;
55
62
  /** Prisma @updatedAt attribute */
56
63
  isUpdatedAt?: boolean;
57
64
  }
@@ -82,6 +89,7 @@ declare const _SITEPING_MODELS: {
82
89
  };
83
90
  readonly projectName: {
84
91
  readonly type: "String";
92
+ readonly maxLength: 191;
85
93
  };
86
94
  readonly type: {
87
95
  readonly type: "String";
@@ -89,6 +97,7 @@ declare const _SITEPING_MODELS: {
89
97
  readonly message: {
90
98
  readonly type: "String";
91
99
  readonly nativeType: "Text";
100
+ readonly maxLength: 5000;
92
101
  };
93
102
  readonly status: {
94
103
  readonly type: "String";
@@ -96,15 +105,18 @@ declare const _SITEPING_MODELS: {
96
105
  };
97
106
  readonly url: {
98
107
  readonly type: "String";
108
+ readonly maxLength: 2000;
99
109
  };
100
110
  readonly urlPattern: {
101
111
  readonly type: "String";
102
112
  readonly optional: true;
113
+ readonly maxLength: 2000;
103
114
  };
104
115
  readonly screenshotUrl: {
105
116
  readonly type: "String";
106
117
  readonly optional: true;
107
118
  readonly nativeType: "Text";
119
+ readonly maxLength: 1500000;
108
120
  };
109
121
  readonly screenshotRegion: {
110
122
  readonly type: "Json";
@@ -116,19 +128,24 @@ declare const _SITEPING_MODELS: {
116
128
  };
117
129
  readonly viewport: {
118
130
  readonly type: "String";
131
+ readonly maxLength: 50;
119
132
  };
120
133
  readonly userAgent: {
121
134
  readonly type: "String";
135
+ readonly maxLength: 500;
122
136
  };
123
137
  readonly authorName: {
124
138
  readonly type: "String";
139
+ readonly maxLength: 200;
125
140
  };
126
141
  readonly authorEmail: {
127
142
  readonly type: "String";
143
+ readonly maxLength: 200;
128
144
  };
129
145
  readonly clientId: {
130
146
  readonly type: "String";
131
147
  readonly isUnique: true;
148
+ readonly maxLength: 191;
132
149
  };
133
150
  readonly resolvedAt: {
134
151
  readonly type: "DateTime";
@@ -188,40 +205,50 @@ declare const _SITEPING_MODELS: {
188
205
  readonly cssSelector: {
189
206
  readonly type: "String";
190
207
  readonly nativeType: "Text";
208
+ readonly maxLength: 2000;
191
209
  };
192
210
  readonly xpath: {
193
211
  readonly type: "String";
194
212
  readonly nativeType: "Text";
213
+ readonly maxLength: 2000;
195
214
  };
196
215
  readonly textSnippet: {
197
216
  readonly type: "String";
198
217
  readonly nativeType: "Text";
218
+ readonly maxLength: 500;
199
219
  };
200
220
  readonly elementTag: {
201
221
  readonly type: "String";
222
+ readonly maxLength: 191;
202
223
  };
203
224
  readonly elementId: {
204
225
  readonly type: "String";
205
226
  readonly optional: true;
227
+ readonly maxLength: 191;
206
228
  };
207
229
  readonly textPrefix: {
208
230
  readonly type: "String";
209
231
  readonly nativeType: "Text";
232
+ readonly maxLength: 200;
210
233
  };
211
234
  readonly textSuffix: {
212
235
  readonly type: "String";
213
236
  readonly nativeType: "Text";
237
+ readonly maxLength: 200;
214
238
  };
215
239
  readonly fingerprint: {
216
240
  readonly type: "String";
241
+ readonly maxLength: 200;
217
242
  };
218
243
  readonly neighborText: {
219
244
  readonly type: "String";
220
245
  readonly nativeType: "Text";
246
+ readonly maxLength: 500;
221
247
  };
222
248
  readonly anchorKey: {
223
249
  readonly type: "String";
224
250
  readonly optional: true;
251
+ readonly maxLength: 200;
225
252
  };
226
253
  readonly xPct: {
227
254
  readonly type: "Float";
@@ -283,12 +310,15 @@ declare const _SITEPING_MODELS: {
283
310
  readonly body: {
284
311
  readonly type: "String";
285
312
  readonly nativeType: "Text";
313
+ readonly maxLength: 5000;
286
314
  };
287
315
  readonly authorName: {
288
316
  readonly type: "String";
317
+ readonly maxLength: 200;
289
318
  };
290
319
  readonly authorEmail: {
291
320
  readonly type: "String";
321
+ readonly maxLength: 200;
292
322
  };
293
323
  readonly authorRole: {
294
324
  readonly type: "String";
@@ -297,6 +327,7 @@ declare const _SITEPING_MODELS: {
297
327
  readonly clientId: {
298
328
  readonly type: "String";
299
329
  readonly isUnique: true;
330
+ readonly maxLength: 191;
300
331
  };
301
332
  readonly createdAt: {
302
333
  readonly type: "DateTime";
package/dist/schema.d.ts CHANGED
@@ -52,6 +52,13 @@ export interface FieldDef {
52
52
  isUnique?: boolean;
53
53
  /** Prisma native type attribute (e.g. "Text" for @db.Text) — used for MySQL compatibility on long strings */
54
54
  nativeType?: PrismaNativeType;
55
+ /**
56
+ * Longest value the HTTP handler accepts for this column, in characters —
57
+ * set on every `String` column filled from a request body, and checked
58
+ * against the handler's own caps. The CLI sizes the column from it where a
59
+ * plain `String` is shorter: on MySQL, a `VARCHAR(191)`.
60
+ */
61
+ maxLength?: number;
55
62
  /** Prisma @updatedAt attribute */
56
63
  isUpdatedAt?: boolean;
57
64
  }
@@ -82,6 +89,7 @@ declare const _SITEPING_MODELS: {
82
89
  };
83
90
  readonly projectName: {
84
91
  readonly type: "String";
92
+ readonly maxLength: 191;
85
93
  };
86
94
  readonly type: {
87
95
  readonly type: "String";
@@ -89,6 +97,7 @@ declare const _SITEPING_MODELS: {
89
97
  readonly message: {
90
98
  readonly type: "String";
91
99
  readonly nativeType: "Text";
100
+ readonly maxLength: 5000;
92
101
  };
93
102
  readonly status: {
94
103
  readonly type: "String";
@@ -96,15 +105,18 @@ declare const _SITEPING_MODELS: {
96
105
  };
97
106
  readonly url: {
98
107
  readonly type: "String";
108
+ readonly maxLength: 2000;
99
109
  };
100
110
  readonly urlPattern: {
101
111
  readonly type: "String";
102
112
  readonly optional: true;
113
+ readonly maxLength: 2000;
103
114
  };
104
115
  readonly screenshotUrl: {
105
116
  readonly type: "String";
106
117
  readonly optional: true;
107
118
  readonly nativeType: "Text";
119
+ readonly maxLength: 1500000;
108
120
  };
109
121
  readonly screenshotRegion: {
110
122
  readonly type: "Json";
@@ -116,19 +128,24 @@ declare const _SITEPING_MODELS: {
116
128
  };
117
129
  readonly viewport: {
118
130
  readonly type: "String";
131
+ readonly maxLength: 50;
119
132
  };
120
133
  readonly userAgent: {
121
134
  readonly type: "String";
135
+ readonly maxLength: 500;
122
136
  };
123
137
  readonly authorName: {
124
138
  readonly type: "String";
139
+ readonly maxLength: 200;
125
140
  };
126
141
  readonly authorEmail: {
127
142
  readonly type: "String";
143
+ readonly maxLength: 200;
128
144
  };
129
145
  readonly clientId: {
130
146
  readonly type: "String";
131
147
  readonly isUnique: true;
148
+ readonly maxLength: 191;
132
149
  };
133
150
  readonly resolvedAt: {
134
151
  readonly type: "DateTime";
@@ -188,40 +205,50 @@ declare const _SITEPING_MODELS: {
188
205
  readonly cssSelector: {
189
206
  readonly type: "String";
190
207
  readonly nativeType: "Text";
208
+ readonly maxLength: 2000;
191
209
  };
192
210
  readonly xpath: {
193
211
  readonly type: "String";
194
212
  readonly nativeType: "Text";
213
+ readonly maxLength: 2000;
195
214
  };
196
215
  readonly textSnippet: {
197
216
  readonly type: "String";
198
217
  readonly nativeType: "Text";
218
+ readonly maxLength: 500;
199
219
  };
200
220
  readonly elementTag: {
201
221
  readonly type: "String";
222
+ readonly maxLength: 191;
202
223
  };
203
224
  readonly elementId: {
204
225
  readonly type: "String";
205
226
  readonly optional: true;
227
+ readonly maxLength: 191;
206
228
  };
207
229
  readonly textPrefix: {
208
230
  readonly type: "String";
209
231
  readonly nativeType: "Text";
232
+ readonly maxLength: 200;
210
233
  };
211
234
  readonly textSuffix: {
212
235
  readonly type: "String";
213
236
  readonly nativeType: "Text";
237
+ readonly maxLength: 200;
214
238
  };
215
239
  readonly fingerprint: {
216
240
  readonly type: "String";
241
+ readonly maxLength: 200;
217
242
  };
218
243
  readonly neighborText: {
219
244
  readonly type: "String";
220
245
  readonly nativeType: "Text";
246
+ readonly maxLength: 500;
221
247
  };
222
248
  readonly anchorKey: {
223
249
  readonly type: "String";
224
250
  readonly optional: true;
251
+ readonly maxLength: 200;
225
252
  };
226
253
  readonly xPct: {
227
254
  readonly type: "Float";
@@ -283,12 +310,15 @@ declare const _SITEPING_MODELS: {
283
310
  readonly body: {
284
311
  readonly type: "String";
285
312
  readonly nativeType: "Text";
313
+ readonly maxLength: 5000;
286
314
  };
287
315
  readonly authorName: {
288
316
  readonly type: "String";
317
+ readonly maxLength: 200;
289
318
  };
290
319
  readonly authorEmail: {
291
320
  readonly type: "String";
321
+ readonly maxLength: 200;
292
322
  };
293
323
  readonly authorRole: {
294
324
  readonly type: "String";
@@ -297,6 +327,7 @@ declare const _SITEPING_MODELS: {
297
327
  readonly clientId: {
298
328
  readonly type: "String";
299
329
  readonly isUnique: true;
330
+ readonly maxLength: 191;
300
331
  };
301
332
  readonly createdAt: {
302
333
  readonly type: "DateTime";
@@ -42,26 +42,24 @@ export interface ScreenshotStorage {
42
42
  * Adapters call this synchronously inside `createFeedback` — keep it
43
43
  * fast or move to a queue if needed.
44
44
  *
45
- * `ctx.feedbackId` identifies the upload. Adapters that upload before the
46
- * record exists pass the *client-generated* `clientId` (Prisma); the
47
- * Drizzle adapter passes the server-generated id the create attempt will
48
- * insert the record under.
45
+ * `ctx.feedbackId` is the id the feedback is stored under. The record does
46
+ * not exist yet, so the store draws it before the upload — fresh for each
47
+ * create attempt, never client-supplied — and inserts the row under it.
48
+ * Two submissions of one `clientId` therefore upload under two ids, and
49
+ * only one of them is stored.
49
50
  *
50
51
  * **URL ownership:** every call must return a URL no other call returns —
51
52
  * key the object by a fresh random value (`crypto.randomUUID()`, as the
52
53
  * example does), never by `ctx.feedbackId` alone nor by a content hash.
53
54
  * Adapters treat each URL as the property of the one record that stores it
54
55
  * and may pass it to `delete` once that record is deleted, or once its
55
- * create lost a race: two submissions of one `clientId` both upload, and
56
- * the object of the one that is not stored is deleted. Under Prisma,
57
- * `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed
58
- * by it is rewritten by every replay — a retry, or anyone who learns the
59
- * id — and a URL shared by several records lets deleting one remove the
60
- * object another still points at.
56
+ * create lost a race: the object of the submission that is not stored is
57
+ * deleted, and a URL shared by several records would let deleting one
58
+ * remove the object another still points at.
61
59
  *
62
- * **Security note:** treat `ctx.feedbackId` as attacker-controlled:
63
- * sanitize before using it in filesystem paths or object keys, even though
64
- * server adapters validate its shape upstream.
60
+ * **Security note:** the bundled stores generate `ctx.feedbackId`, but a
61
+ * third-party one may not honour the contract: sanitize it before using it
62
+ * in a filesystem path or an object key.
65
63
  */
66
64
  upload(dataUrl: string, ctx: {
67
65
  feedbackId: string;
@@ -72,10 +70,9 @@ export interface ScreenshotStorage {
72
70
  /**
73
71
  * Optional cleanup hook called when the feedback is deleted, and for an
74
72
  * object uploaded by a create whose record was not stored: one rejected
75
- * because its `clientId` is already stored, or — in adapters that key
76
- * uploads per attempt, like Drizzle — one whose insert failed. An object a
77
- * stored row still references (a deterministic key reused by the replay)
78
- * is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
73
+ * because its `clientId` is already stored, or one whose insert failed. An
74
+ * object a stored row still references (a storage that broke URL
75
+ * ownership) is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
79
76
  * Adapters call this best-effort and swallow errors — orphaned objects are
80
77
  * preferred over failed deletes.
81
78
  */
@@ -42,26 +42,24 @@ export interface ScreenshotStorage {
42
42
  * Adapters call this synchronously inside `createFeedback` — keep it
43
43
  * fast or move to a queue if needed.
44
44
  *
45
- * `ctx.feedbackId` identifies the upload. Adapters that upload before the
46
- * record exists pass the *client-generated* `clientId` (Prisma); the
47
- * Drizzle adapter passes the server-generated id the create attempt will
48
- * insert the record under.
45
+ * `ctx.feedbackId` is the id the feedback is stored under. The record does
46
+ * not exist yet, so the store draws it before the upload — fresh for each
47
+ * create attempt, never client-supplied — and inserts the row under it.
48
+ * Two submissions of one `clientId` therefore upload under two ids, and
49
+ * only one of them is stored.
49
50
  *
50
51
  * **URL ownership:** every call must return a URL no other call returns —
51
52
  * key the object by a fresh random value (`crypto.randomUUID()`, as the
52
53
  * example does), never by `ctx.feedbackId` alone nor by a content hash.
53
54
  * Adapters treat each URL as the property of the one record that stores it
54
55
  * and may pass it to `delete` once that record is deleted, or once its
55
- * create lost a race: two submissions of one `clientId` both upload, and
56
- * the object of the one that is not stored is deleted. Under Prisma,
57
- * `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed
58
- * by it is rewritten by every replay — a retry, or anyone who learns the
59
- * id — and a URL shared by several records lets deleting one remove the
60
- * object another still points at.
56
+ * create lost a race: the object of the submission that is not stored is
57
+ * deleted, and a URL shared by several records would let deleting one
58
+ * remove the object another still points at.
61
59
  *
62
- * **Security note:** treat `ctx.feedbackId` as attacker-controlled:
63
- * sanitize before using it in filesystem paths or object keys, even though
64
- * server adapters validate its shape upstream.
60
+ * **Security note:** the bundled stores generate `ctx.feedbackId`, but a
61
+ * third-party one may not honour the contract: sanitize it before using it
62
+ * in a filesystem path or an object key.
65
63
  */
66
64
  upload(dataUrl: string, ctx: {
67
65
  feedbackId: string;
@@ -72,10 +70,9 @@ export interface ScreenshotStorage {
72
70
  /**
73
71
  * Optional cleanup hook called when the feedback is deleted, and for an
74
72
  * object uploaded by a create whose record was not stored: one rejected
75
- * because its `clientId` is already stored, or — in adapters that key
76
- * uploads per attempt, like Drizzle — one whose insert failed. An object a
77
- * stored row still references (a deterministic key reused by the replay)
78
- * is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
73
+ * because its `clientId` is already stored, or one whose insert failed. An
74
+ * object a stored row still references (a storage that broke URL
75
+ * ownership) is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
79
76
  * Adapters call this best-effort and swallow errors — orphaned objects are
80
77
  * preferred over failed deletes.
81
78
  */