@siteping/adapter-drizzle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +40 -0
  2. package/dist/chunk-6G6UAERF.js +814 -0
  3. package/dist/chunk-6G6UAERF.js.map +1 -0
  4. package/dist/chunk-ICY2AW6S.js +11 -0
  5. package/dist/chunk-ICY2AW6S.js.map +1 -0
  6. package/dist/concurrency.d.cts +14 -0
  7. package/dist/concurrency.d.ts +14 -0
  8. package/dist/deep-link.d.cts +14 -0
  9. package/dist/deep-link.d.ts +14 -0
  10. package/dist/email.d.cts +22 -0
  11. package/dist/email.d.ts +22 -0
  12. package/dist/errors.d.cts +55 -0
  13. package/dist/errors.d.ts +55 -0
  14. package/dist/filters.d.cts +82 -0
  15. package/dist/filters.d.ts +82 -0
  16. package/dist/i18n.d.cts +85 -0
  17. package/dist/i18n.d.ts +85 -0
  18. package/dist/index.cjs +33 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +45 -0
  21. package/dist/index.d.ts +45 -0
  22. package/dist/index.js +7 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/libsql.cjs +1045 -0
  25. package/dist/libsql.cjs.map +1 -0
  26. package/dist/libsql.d.cts +1015 -0
  27. package/dist/libsql.d.ts +1015 -0
  28. package/dist/libsql.js +254 -0
  29. package/dist/libsql.js.map +1 -0
  30. package/dist/pg.cjs +1079 -0
  31. package/dist/pg.cjs.map +1 -0
  32. package/dist/pg.d.cts +968 -0
  33. package/dist/pg.d.ts +968 -0
  34. package/dist/pg.js +298 -0
  35. package/dist/pg.js.map +1 -0
  36. package/dist/schema.d.cts +317 -0
  37. package/dist/schema.d.ts +317 -0
  38. package/dist/screenshot-storage.d.cts +103 -0
  39. package/dist/screenshot-storage.d.ts +103 -0
  40. package/dist/siteping-core.d.cts +20 -0
  41. package/dist/siteping-core.d.ts +20 -0
  42. package/dist/store-helpers.d.cts +140 -0
  43. package/dist/store-helpers.d.ts +140 -0
  44. package/dist/type-utils.d.cts +66 -0
  45. package/dist/type-utils.d.ts +66 -0
  46. package/dist/types.d.cts +1206 -0
  47. package/dist/types.d.ts +1206 -0
  48. package/dist/wire.d.cts +68 -0
  49. package/dist/wire.d.ts +68 -0
  50. package/package.json +89 -0
package/dist/libsql.js ADDED
@@ -0,0 +1,254 @@
1
+ import {
2
+ CASE_INSENSITIVE_LIKE_OPERATOR,
3
+ DRIZZLE_STORE_MESSAGE_PREFIX,
4
+ DrizzleSitepingStore,
5
+ GREATEST_VALUE_FUNCTION,
6
+ StoreDuplicateError,
7
+ StoreLimitError,
8
+ StoreNotFoundError,
9
+ StorePersistenceError,
10
+ buildFeedbackWhere,
11
+ deletedFeedbackColumns,
12
+ feedbackRecordColumns,
13
+ guardedCommentInsert,
14
+ isStorePersistence,
15
+ monotonicUpdatedAt,
16
+ newestFeedbackFirst,
17
+ primaryOf,
18
+ recordColumns,
19
+ selectValues,
20
+ toDeletedFeedbacks,
21
+ withSearchableMessage
22
+ } from "./chunk-6G6UAERF.js";
23
+ import {
24
+ DEFAULT_SITEPING_TABLE_NAMES
25
+ } from "./chunk-ICY2AW6S.js";
26
+
27
+ // src/libsql/index.ts
28
+ import { is } from "drizzle-orm";
29
+ import { LibSQLDatabase } from "drizzle-orm/libsql/driver-core";
30
+
31
+ // src/libsql/gateway.ts
32
+ import { and, count, eq, getTableColumns, inArray, sql } from "drizzle-orm";
33
+ function createLibSQLGateway(db, { sitepingFeedbacks, sitepingAnnotations, sitepingComments }) {
34
+ const whereClause = (filter) => buildFeedbackWhere(sitepingFeedbacks, filter, CASE_INSENSITIVE_LIKE_OPERATOR.sqlite);
35
+ const feedbackColumns = feedbackRecordColumns(getTableColumns(sitepingFeedbacks));
36
+ const commentColumns = recordColumns(getTableColumns(sitepingComments));
37
+ const countMatching = async (where) => {
38
+ const [totals] = await db.select({ total: count() }).from(sitepingFeedbacks).where(where);
39
+ return totals?.total ?? 0;
40
+ };
41
+ const insertionOrder = sql`${sitepingFeedbacks}.rowid`;
42
+ return {
43
+ async insertFeedback(feedback, annotations) {
44
+ const insertFeedbackRow = db.insert(sitepingFeedbacks).values(withSearchableMessage(feedback)).onConflictDoNothing({ target: sitepingFeedbacks.clientId }).returning({ id: sitepingFeedbacks.id });
45
+ if (annotations.length === 0) return (await insertFeedbackRow).length > 0;
46
+ const [inserted] = await db.batch([
47
+ insertFeedbackRow,
48
+ db.insert(sitepingAnnotations).select(
49
+ selectValues(
50
+ sitepingAnnotations,
51
+ annotations.map((annotation, position) => ({ ...annotation, position })),
52
+ sql`EXISTS (SELECT 1 FROM ${sitepingFeedbacks} WHERE ${sitepingFeedbacks.id} = ${feedback.id})`
53
+ )
54
+ )
55
+ ]);
56
+ return inserted.length > 0;
57
+ },
58
+ async findFeedbacks(filter, { limit, offset }) {
59
+ const where = whereClause(filter);
60
+ const [rows, total] = await Promise.all([
61
+ db.select(feedbackColumns).from(sitepingFeedbacks).where(where).orderBy(...newestFeedbackFirst(sitepingFeedbacks.createdAt, insertionOrder)).limit(limit).offset(offset),
62
+ countMatching(where)
63
+ ]);
64
+ return { rows, total };
65
+ },
66
+ async countFeedbacks(filter) {
67
+ return countMatching(whereClause(filter));
68
+ },
69
+ async findAnnotations(feedbackIds) {
70
+ return db.select(recordColumns(getTableColumns(sitepingAnnotations))).from(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, [...feedbackIds])).orderBy(sitepingAnnotations.createdAt, sitepingAnnotations.position);
71
+ },
72
+ async findComments(feedbackIds) {
73
+ return db.select(commentColumns).from(sitepingComments).where(inArray(sitepingComments.feedbackId, [...feedbackIds])).orderBy(sitepingComments.createdAt, sitepingComments.position);
74
+ },
75
+ async findCommentByClientId(clientId) {
76
+ const [row] = await db.select(commentColumns).from(sitepingComments).where(eq(sitepingComments.clientId, clientId)).limit(1);
77
+ return row ?? null;
78
+ },
79
+ async insertComment(comment, maxComments) {
80
+ const { values, condition } = guardedCommentInsert(
81
+ { feedbacks: sitepingFeedbacks, comments: sitepingComments },
82
+ comment,
83
+ maxComments
84
+ );
85
+ const inserted = await db.insert(sitepingComments).select(selectValues(sitepingComments, [values], condition)).onConflictDoNothing({ target: sitepingComments.clientId }).returning({ id: sitepingComments.id });
86
+ return inserted.length > 0;
87
+ },
88
+ async deleteComment(feedbackId, commentId) {
89
+ const deleted = await db.delete(sitepingComments).where(and(eq(sitepingComments.id, commentId), eq(sitepingComments.feedbackId, feedbackId))).returning({ id: sitepingComments.id });
90
+ return deleted.length > 0;
91
+ },
92
+ async findByClientId(clientId) {
93
+ const [row] = await db.select(feedbackColumns).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.clientId, clientId)).limit(1);
94
+ return row ?? null;
95
+ },
96
+ async findProjectName(id) {
97
+ const [row] = await db.select({ projectName: sitepingFeedbacks.projectName }).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.id, id)).limit(1);
98
+ return row?.projectName ?? null;
99
+ },
100
+ async updateStatus(id, { status, resolvedAt, updatedAt }) {
101
+ const [row] = await db.update(sitepingFeedbacks).set({
102
+ status,
103
+ resolvedAt,
104
+ updatedAt: monotonicUpdatedAt(sitepingFeedbacks, updatedAt, GREATEST_VALUE_FUNCTION.sqlite)
105
+ }).where(eq(sitepingFeedbacks.id, id)).returning(feedbackColumns);
106
+ return row ?? null;
107
+ },
108
+ async deleteById(id, options) {
109
+ const [, , deleted] = await db.batch([
110
+ db.delete(sitepingAnnotations).where(eq(sitepingAnnotations.feedbackId, id)),
111
+ db.delete(sitepingComments).where(eq(sitepingComments.feedbackId, id)),
112
+ db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.id, id)).returning(deletedFeedbackColumns(sitepingFeedbacks, options))
113
+ ]);
114
+ return deleted.length > 0 ? toDeletedFeedbacks(deleted) : null;
115
+ },
116
+ async deleteByProject(projectName) {
117
+ const projectFeedbackIds = db.select({ id: sitepingFeedbacks.id }).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName));
118
+ await db.batch([
119
+ db.delete(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, projectFeedbackIds)),
120
+ db.delete(sitepingComments).where(inArray(sitepingComments.feedbackId, projectFeedbackIds)),
121
+ db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName))
122
+ ]);
123
+ },
124
+ async deleteProjectChunk(projectName, chunkSize) {
125
+ const chunkIds = db.select({ id: sitepingFeedbacks.id }).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName)).orderBy(sitepingFeedbacks.id).limit(chunkSize);
126
+ const [, , deleted] = await db.batch([
127
+ db.delete(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, chunkIds)),
128
+ db.delete(sitepingComments).where(inArray(sitepingComments.feedbackId, chunkIds)),
129
+ db.delete(sitepingFeedbacks).where(inArray(sitepingFeedbacks.id, chunkIds)).returning(deletedFeedbackColumns(sitepingFeedbacks, { collectScreenshotUrls: true }))
130
+ ]);
131
+ return toDeletedFeedbacks(deleted);
132
+ },
133
+ async findReferencedScreenshotUrls(screenshotUrls) {
134
+ const rows = await db.selectDistinct({ screenshotUrl: sitepingFeedbacks.screenshotUrl }).from(sitepingFeedbacks).where(inArray(sitepingFeedbacks.screenshotUrl, [...screenshotUrls]));
135
+ return new Set(rows.flatMap((row) => row.screenshotUrl === null ? [] : [row.screenshotUrl]));
136
+ }
137
+ };
138
+ }
139
+
140
+ // src/libsql/tables.ts
141
+ import { index, integer, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
142
+ function createSitepingSqliteTables(names = DEFAULT_SITEPING_TABLE_NAMES) {
143
+ const sitepingFeedbacks = sqliteTable(
144
+ names.feedbacks,
145
+ {
146
+ id: text("id").primaryKey(),
147
+ projectName: text("project_name").notNull(),
148
+ type: text("type").$type().notNull(),
149
+ message: text("message").notNull(),
150
+ status: text("status").$type().notNull().default("open"),
151
+ url: text("url").notNull(),
152
+ urlPattern: text("url_pattern"),
153
+ screenshotUrl: text("screenshot_url"),
154
+ screenshotRegion: text("screenshot_region", { mode: "json" }).$type(),
155
+ diagnostics: text("diagnostics", { mode: "json" }).$type(),
156
+ viewport: text("viewport").notNull(),
157
+ userAgent: text("user_agent").notNull(),
158
+ authorName: text("author_name").notNull(),
159
+ authorEmail: text("author_email").notNull(),
160
+ clientId: text("client_id").notNull(),
161
+ resolvedAt: integer("resolved_at", { mode: "timestamp_ms" }),
162
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
163
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
164
+ // `message` lowercased in JavaScript (`String.prototype.toLowerCase`, Unicode-aware), the
165
+ // column the text search reads: SQLite's LIKE folds only ASCII case, so `Échec` would not
166
+ // match `échec`. The store fills it on insert (a feedback's message never changes); rows
167
+ // written by the host application stay NULL and the search falls back to `message` with
168
+ // ASCII-only folding. Internal — never part of the feedback record.
169
+ messageSearch: text("message_search")
170
+ },
171
+ (table) => [
172
+ uniqueIndex(`${names.feedbacks}_client_id_key`).on(table.clientId),
173
+ index(`${names.feedbacks}_project_status_created_idx`).on(table.projectName, table.status, table.createdAt),
174
+ index(`${names.feedbacks}_project_url_idx`).on(table.projectName, table.url)
175
+ ]
176
+ );
177
+ const sitepingAnnotations = sqliteTable(
178
+ names.annotations,
179
+ {
180
+ id: text("id").primaryKey(),
181
+ feedbackId: text("feedback_id").notNull().references(() => sitepingFeedbacks.id, { onDelete: "cascade" }),
182
+ cssSelector: text("css_selector").notNull(),
183
+ xpath: text("xpath").notNull(),
184
+ textSnippet: text("text_snippet").notNull(),
185
+ elementTag: text("element_tag").notNull(),
186
+ elementId: text("element_id"),
187
+ textPrefix: text("text_prefix").notNull(),
188
+ textSuffix: text("text_suffix").notNull(),
189
+ fingerprint: text("fingerprint").notNull(),
190
+ neighborText: text("neighbor_text").notNull(),
191
+ anchorKey: text("anchor_key"),
192
+ xPct: real("x_pct").notNull(),
193
+ yPct: real("y_pct").notNull(),
194
+ wPct: real("w_pct").notNull(),
195
+ hPct: real("h_pct").notNull(),
196
+ scrollX: real("scroll_x").notNull(),
197
+ scrollY: real("scroll_y").notNull(),
198
+ viewportW: integer("viewport_w").notNull(),
199
+ viewportH: integer("viewport_h").notNull(),
200
+ devicePixelRatio: real("device_pixel_ratio").notNull().default(1),
201
+ // Submission index within the feedback: `annotations[0]` is the primary anchor, and
202
+ // every annotation of a feedback shares one `createdAt`. Rows the host application
203
+ // inserts without it default to 0 and keep their `createdAt` order.
204
+ position: integer("position").notNull().default(0),
205
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull()
206
+ },
207
+ (table) => [index(`${names.annotations}_feedback_id_idx`).on(table.feedbackId)]
208
+ );
209
+ const sitepingComments = sqliteTable(
210
+ names.comments,
211
+ {
212
+ id: text("id").primaryKey(),
213
+ feedbackId: text("feedback_id").notNull().references(() => sitepingFeedbacks.id, { onDelete: "cascade" }),
214
+ body: text("body").notNull(),
215
+ authorName: text("author_name").notNull(),
216
+ authorEmail: text("author_email").notNull(),
217
+ authorRole: text("author_role").$type().notNull().default("client"),
218
+ clientId: text("client_id").notNull(),
219
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
220
+ // Posting index within the thread, assigned by the insert itself: breaks `createdAt`
221
+ // ties so a thread reads in posting order. Rows the host application inserts without
222
+ // it default to 0 and keep their `createdAt` order.
223
+ position: integer("position").notNull().default(0)
224
+ },
225
+ (table) => [
226
+ uniqueIndex(`${names.comments}_client_id_key`).on(table.clientId),
227
+ index(`${names.comments}_feedback_created_idx`).on(table.feedbackId, table.createdAt)
228
+ ]
229
+ );
230
+ return { sitepingFeedbacks, sitepingAnnotations, sitepingComments };
231
+ }
232
+
233
+ // src/libsql/index.ts
234
+ function createLibSQLSitepingStore(db, options = {}) {
235
+ const primary = primaryOf(db);
236
+ if (!is(primary, LibSQLDatabase)) {
237
+ throw new TypeError(
238
+ `${DRIZZLE_STORE_MESSAGE_PREFIX}: createLibSQLSitepingStore needs a database from drizzle-orm/libsql \u2014 other SQLite drivers (Cloudflare D1, better-sqlite3\u2026) are not supported`
239
+ );
240
+ }
241
+ const { tables = createSitepingSqliteTables(), ...storeOptions } = options;
242
+ return new DrizzleSitepingStore(createLibSQLGateway(primary, tables), storeOptions);
243
+ }
244
+ export {
245
+ DEFAULT_SITEPING_TABLE_NAMES,
246
+ StoreDuplicateError,
247
+ StoreLimitError,
248
+ StoreNotFoundError,
249
+ StorePersistenceError,
250
+ createLibSQLSitepingStore,
251
+ createSitepingSqliteTables,
252
+ isStorePersistence
253
+ };
254
+ //# sourceMappingURL=libsql.js.map
@@ -0,0 +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":[]}