@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/pg.js ADDED
@@ -0,0 +1,298 @@
1
+ import {
2
+ CASE_INSENSITIVE_LIKE_OPERATOR,
3
+ DRIZZLE_STORE_MESSAGE_PREFIX,
4
+ DrizzleSitepingStore,
5
+ GREATEST_VALUE_FUNCTION,
6
+ INSERTED_ANNOTATIONS_CTE_ALIAS,
7
+ INSERTED_FEEDBACK_CTE_ALIAS,
8
+ POSTGRES_IDENTIFIER_MAX_BYTES,
9
+ StoreDuplicateError,
10
+ StoreLimitError,
11
+ StoreNotFoundError,
12
+ StorePersistenceError,
13
+ buildFeedbackWhere,
14
+ deletedFeedbackColumns,
15
+ feedbackRecordColumns,
16
+ guardedCommentInsert,
17
+ isStorePersistence,
18
+ monotonicUpdatedAt,
19
+ newestFeedbackFirst,
20
+ primaryOf,
21
+ recordColumns,
22
+ selectValues,
23
+ toDeletedFeedbacks,
24
+ withSearchableMessage
25
+ } from "./chunk-6G6UAERF.js";
26
+ import {
27
+ DEFAULT_SITEPING_TABLE_NAMES
28
+ } from "./chunk-ICY2AW6S.js";
29
+
30
+ // src/pg/gateway.ts
31
+ import { and, count, eq, getTableColumns, inArray, sql } from "drizzle-orm";
32
+ function castToColumnType(param, column) {
33
+ return sql`CAST(${param} AS ${sql.raw(column.getSQLType())})`;
34
+ }
35
+ function createPgGateway(db, { sitepingFeedbacks, sitepingAnnotations, sitepingComments }) {
36
+ const whereClause = (filter) => buildFeedbackWhere(sitepingFeedbacks, filter, CASE_INSENSITIVE_LIKE_OPERATOR.postgres);
37
+ const feedbackColumns = feedbackRecordColumns(getTableColumns(sitepingFeedbacks));
38
+ const commentColumns = recordColumns(getTableColumns(sitepingComments));
39
+ const countMatching = async (where) => {
40
+ const [totals] = await db.select({ total: count() }).from(sitepingFeedbacks).where(where);
41
+ return totals?.total ?? 0;
42
+ };
43
+ return {
44
+ /**
45
+ * One statement, no interactive transaction (Neon HTTP has none): the
46
+ * feedback insert is a CTE, and the annotation insert selects from it —
47
+ * so annotations land only with the feedback row, atomically.
48
+ */
49
+ async insertFeedback(feedback, annotations) {
50
+ const insertedFeedback = db.$with(INSERTED_FEEDBACK_CTE_ALIAS).as(
51
+ db.insert(sitepingFeedbacks).values(withSearchableMessage(feedback)).onConflictDoNothing({ target: sitepingFeedbacks.clientId }).returning({ id: sitepingFeedbacks.id })
52
+ );
53
+ const statements = [insertedFeedback];
54
+ if (annotations.length > 0) {
55
+ statements.push(
56
+ db.$with(INSERTED_ANNOTATIONS_CTE_ALIAS).as(
57
+ db.insert(sitepingAnnotations).select(
58
+ selectValues(
59
+ sitepingAnnotations,
60
+ annotations.map((annotation, position) => ({ ...annotation, position })),
61
+ sql`EXISTS (SELECT 1 FROM ${insertedFeedback})`,
62
+ castToColumnType
63
+ )
64
+ )
65
+ )
66
+ );
67
+ }
68
+ const inserted = await db.with(...statements).select({ id: insertedFeedback.id }).from(insertedFeedback);
69
+ return inserted.length > 0;
70
+ },
71
+ async findFeedbacks(filter, { limit, offset }) {
72
+ const where = whereClause(filter);
73
+ const [rows, total] = await Promise.all([
74
+ db.select(feedbackColumns).from(sitepingFeedbacks).where(where).orderBy(...newestFeedbackFirst(sitepingFeedbacks.createdAt, sitepingFeedbacks.creationSequence)).limit(limit).offset(offset),
75
+ countMatching(where)
76
+ ]);
77
+ return { rows, total };
78
+ },
79
+ async countFeedbacks(filter) {
80
+ return countMatching(whereClause(filter));
81
+ },
82
+ async findAnnotations(feedbackIds) {
83
+ return db.select(recordColumns(getTableColumns(sitepingAnnotations))).from(sitepingAnnotations).where(inArray(sitepingAnnotations.feedbackId, [...feedbackIds])).orderBy(sitepingAnnotations.createdAt, sitepingAnnotations.position);
84
+ },
85
+ async findComments(feedbackIds) {
86
+ return db.select(commentColumns).from(sitepingComments).where(inArray(sitepingComments.feedbackId, [...feedbackIds])).orderBy(sitepingComments.createdAt, sitepingComments.position);
87
+ },
88
+ async findCommentByClientId(clientId) {
89
+ const [row] = await db.select(commentColumns).from(sitepingComments).where(eq(sitepingComments.clientId, clientId)).limit(1);
90
+ return row ?? null;
91
+ },
92
+ async insertComment(comment, maxComments) {
93
+ const { values, condition } = guardedCommentInsert(
94
+ { feedbacks: sitepingFeedbacks, comments: sitepingComments },
95
+ comment,
96
+ maxComments
97
+ );
98
+ const inserted = await db.insert(sitepingComments).select(selectValues(sitepingComments, [values], condition, castToColumnType)).onConflictDoNothing({ target: sitepingComments.clientId }).returning({ id: sitepingComments.id });
99
+ return inserted.length > 0;
100
+ },
101
+ async deleteComment(feedbackId, commentId) {
102
+ const deleted = await db.delete(sitepingComments).where(and(eq(sitepingComments.id, commentId), eq(sitepingComments.feedbackId, feedbackId))).returning({ id: sitepingComments.id });
103
+ return deleted.length > 0;
104
+ },
105
+ async findByClientId(clientId) {
106
+ const [row] = await db.select(feedbackColumns).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.clientId, clientId)).limit(1);
107
+ return row ?? null;
108
+ },
109
+ async findProjectName(id) {
110
+ const [row] = await db.select({ projectName: sitepingFeedbacks.projectName }).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.id, id)).limit(1);
111
+ return row?.projectName ?? null;
112
+ },
113
+ async updateStatus(id, { status, resolvedAt, updatedAt }) {
114
+ const [row] = await db.update(sitepingFeedbacks).set({
115
+ status,
116
+ resolvedAt,
117
+ updatedAt: monotonicUpdatedAt(sitepingFeedbacks, updatedAt, GREATEST_VALUE_FUNCTION.postgres)
118
+ }).where(eq(sitepingFeedbacks.id, id)).returning(feedbackColumns);
119
+ return row ?? null;
120
+ },
121
+ async deleteById(id, options) {
122
+ const rows = await db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.id, id)).returning(deletedFeedbackColumns(sitepingFeedbacks, options));
123
+ return rows.length > 0 ? toDeletedFeedbacks(rows) : null;
124
+ },
125
+ // Annotations and comments follow their feedback through the `ON DELETE CASCADE` foreign keys.
126
+ async deleteByProject(projectName) {
127
+ await db.delete(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName));
128
+ },
129
+ async deleteProjectChunk(projectName, chunkSize) {
130
+ const chunkIds = db.select({ id: sitepingFeedbacks.id }).from(sitepingFeedbacks).where(eq(sitepingFeedbacks.projectName, projectName)).orderBy(sitepingFeedbacks.id).limit(chunkSize);
131
+ const rows = await db.delete(sitepingFeedbacks).where(inArray(sitepingFeedbacks.id, chunkIds)).returning(deletedFeedbackColumns(sitepingFeedbacks, { collectScreenshotUrls: true }));
132
+ return toDeletedFeedbacks(rows);
133
+ },
134
+ async findReferencedScreenshotUrls(screenshotUrls) {
135
+ const rows = await db.selectDistinct({ screenshotUrl: sitepingFeedbacks.screenshotUrl }).from(sitepingFeedbacks).where(inArray(sitepingFeedbacks.screenshotUrl, [...screenshotUrls]));
136
+ return new Set(rows.flatMap((row) => row.screenshotUrl === null ? [] : [row.screenshotUrl]));
137
+ }
138
+ };
139
+ }
140
+
141
+ // src/pg/tables.ts
142
+ import {
143
+ bigint,
144
+ doublePrecision,
145
+ foreignKey,
146
+ getTableConfig,
147
+ index,
148
+ integer,
149
+ jsonb,
150
+ pgTable,
151
+ text,
152
+ timestamp,
153
+ uniqueIndex
154
+ } from "drizzle-orm/pg-core";
155
+ function assertIdentifiersFitPostgres(tables) {
156
+ const encoder = new TextEncoder();
157
+ for (const table of tables) {
158
+ const { name, indexes, foreignKeys } = getTableConfig(table);
159
+ const identifiers = [
160
+ name,
161
+ ...indexes.flatMap((tableIndex) => tableIndex.config.name ?? []),
162
+ ...foreignKeys.map((key) => key.getName())
163
+ ];
164
+ for (const identifier of identifiers) {
165
+ const bytes = encoder.encode(identifier).length;
166
+ if (bytes > POSTGRES_IDENTIFIER_MAX_BYTES) {
167
+ throw new RangeError(
168
+ `${DRIZZLE_STORE_MESSAGE_PREFIX}: "${identifier}" is ${bytes} bytes long, over PostgreSQL's ${POSTGRES_IDENTIFIER_MAX_BYTES}-byte identifier limit \u2014 pass shorter table names to createSitepingPgTables`
169
+ );
170
+ }
171
+ }
172
+ }
173
+ }
174
+ function createSitepingPgTables(names = DEFAULT_SITEPING_TABLE_NAMES) {
175
+ const sitepingFeedbacks = pgTable(
176
+ names.feedbacks,
177
+ {
178
+ id: text("id").primaryKey(),
179
+ projectName: text("project_name").notNull(),
180
+ type: text("type").$type().notNull(),
181
+ message: text("message").notNull(),
182
+ status: text("status").$type().notNull().default("open"),
183
+ url: text("url").notNull(),
184
+ urlPattern: text("url_pattern"),
185
+ screenshotUrl: text("screenshot_url"),
186
+ screenshotRegion: jsonb("screenshot_region").$type(),
187
+ diagnostics: jsonb("diagnostics").$type(),
188
+ viewport: text("viewport").notNull(),
189
+ userAgent: text("user_agent").notNull(),
190
+ authorName: text("author_name").notNull(),
191
+ authorEmail: text("author_email").notNull(),
192
+ clientId: text("client_id").notNull(),
193
+ resolvedAt: timestamp("resolved_at", { withTimezone: true, precision: 3 }),
194
+ createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).notNull().defaultNow(),
195
+ updatedAt: timestamp("updated_at", { withTimezone: true, precision: 3 }).notNull().defaultNow(),
196
+ // Database-wide insertion ordinal: breaks `createdAt` ties between rows created in the
197
+ // same millisecond, by any store instance or process — or by the host application
198
+ // inserting directly — so "newest first" and offset pages stay stable. The database
199
+ // assigns it on every insert. Internal — never part of the feedback record.
200
+ creationSequence: bigint("creation_sequence", { mode: "number" }).generatedAlwaysAsIdentity(),
201
+ // `message` lowercased in JavaScript (`String.prototype.toLowerCase`, Unicode-aware), the
202
+ // column the text search reads: `ILIKE` folds case with the collation / `LC_CTYPE`, so under
203
+ // `C` `Échec` would not match `échec`. The store fills it on insert (a feedback's message
204
+ // never changes); rows written by the host application stay NULL and the search falls
205
+ // back to `message ILIKE`. Internal — never part of the feedback record.
206
+ messageSearch: text("message_search")
207
+ },
208
+ (table) => [
209
+ uniqueIndex(`${names.feedbacks}_client_id_key`).on(table.clientId),
210
+ index(`${names.feedbacks}_project_status_created_idx`).on(table.projectName, table.status, table.createdAt),
211
+ index(`${names.feedbacks}_project_url_idx`).on(table.projectName, table.url)
212
+ ]
213
+ );
214
+ const sitepingAnnotations = pgTable(
215
+ names.annotations,
216
+ {
217
+ id: text("id").primaryKey(),
218
+ feedbackId: text("feedback_id").notNull(),
219
+ cssSelector: text("css_selector").notNull(),
220
+ xpath: text("xpath").notNull(),
221
+ textSnippet: text("text_snippet").notNull(),
222
+ elementTag: text("element_tag").notNull(),
223
+ elementId: text("element_id"),
224
+ textPrefix: text("text_prefix").notNull(),
225
+ textSuffix: text("text_suffix").notNull(),
226
+ fingerprint: text("fingerprint").notNull(),
227
+ neighborText: text("neighbor_text").notNull(),
228
+ anchorKey: text("anchor_key"),
229
+ xPct: doublePrecision("x_pct").notNull(),
230
+ yPct: doublePrecision("y_pct").notNull(),
231
+ wPct: doublePrecision("w_pct").notNull(),
232
+ hPct: doublePrecision("h_pct").notNull(),
233
+ scrollX: doublePrecision("scroll_x").notNull(),
234
+ scrollY: doublePrecision("scroll_y").notNull(),
235
+ viewportW: integer("viewport_w").notNull(),
236
+ viewportH: integer("viewport_h").notNull(),
237
+ devicePixelRatio: doublePrecision("device_pixel_ratio").notNull().default(1),
238
+ // Submission index within the feedback: `annotations[0]` is the primary anchor, and
239
+ // every annotation of a feedback shares one `createdAt`. Rows the host application
240
+ // inserts without it default to 0 and keep their `createdAt` order.
241
+ position: integer("position").notNull().default(0),
242
+ createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).notNull().defaultNow()
243
+ },
244
+ (table) => [
245
+ foreignKey({
246
+ name: `${names.annotations}_feedback_id_fk`,
247
+ columns: [table.feedbackId],
248
+ foreignColumns: [sitepingFeedbacks.id]
249
+ }).onDelete("cascade"),
250
+ index(`${names.annotations}_feedback_id_idx`).on(table.feedbackId)
251
+ ]
252
+ );
253
+ const sitepingComments = pgTable(
254
+ names.comments,
255
+ {
256
+ id: text("id").primaryKey(),
257
+ feedbackId: text("feedback_id").notNull(),
258
+ body: text("body").notNull(),
259
+ authorName: text("author_name").notNull(),
260
+ authorEmail: text("author_email").notNull(),
261
+ authorRole: text("author_role").$type().notNull().default("client"),
262
+ clientId: text("client_id").notNull(),
263
+ createdAt: timestamp("created_at", { withTimezone: true, precision: 3 }).notNull().defaultNow(),
264
+ // Posting index within the thread, assigned by the insert itself: breaks `createdAt`
265
+ // ties so a thread reads in posting order. Rows the host application inserts without
266
+ // it default to 0 and keep their `createdAt` order.
267
+ position: integer("position").notNull().default(0)
268
+ },
269
+ (table) => [
270
+ foreignKey({
271
+ name: `${names.comments}_feedback_id_fk`,
272
+ columns: [table.feedbackId],
273
+ foreignColumns: [sitepingFeedbacks.id]
274
+ }).onDelete("cascade"),
275
+ uniqueIndex(`${names.comments}_client_id_key`).on(table.clientId),
276
+ index(`${names.comments}_feedback_created_idx`).on(table.feedbackId, table.createdAt)
277
+ ]
278
+ );
279
+ assertIdentifiersFitPostgres([sitepingFeedbacks, sitepingAnnotations, sitepingComments]);
280
+ return { sitepingFeedbacks, sitepingAnnotations, sitepingComments };
281
+ }
282
+
283
+ // src/pg/index.ts
284
+ function createPgSitepingStore(db, options = {}) {
285
+ const { tables = createSitepingPgTables(), ...storeOptions } = options;
286
+ return new DrizzleSitepingStore(createPgGateway(primaryOf(db), tables), storeOptions);
287
+ }
288
+ export {
289
+ DEFAULT_SITEPING_TABLE_NAMES,
290
+ StoreDuplicateError,
291
+ StoreLimitError,
292
+ StoreNotFoundError,
293
+ StorePersistenceError,
294
+ createPgSitepingStore,
295
+ createSitepingPgTables,
296
+ isStorePersistence
297
+ };
298
+ //# sourceMappingURL=pg.js.map
package/dist/pg.js.map ADDED
@@ -0,0 +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":[]}
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Siteping database models — single source of truth.
3
+ *
4
+ * Used by:
5
+ * - CLI to generate Prisma schema (via prisma-ast)
6
+ * - Adapter for Zod validation
7
+ * - Type exports
8
+ *
9
+ * This is a TS representation, NOT a .prisma file.
10
+ * The CLI generates the actual Prisma schema from this definition.
11
+ */
12
+ /** Prisma scalar types supported by Siteping field definitions. */
13
+ export type PrismaScalarType = "String" | "Boolean" | "Int" | "BigInt" | "Float" | "Decimal" | "DateTime" | "Json" | "Bytes";
14
+ /** Prisma native column hints applied via `@db.<NativeType>`. */
15
+ export type PrismaNativeType = "Text" | "VarChar" | "Char" | "MediumText" | "LongText" | (string & {});
16
+ /** Relation cardinality between two Siteping models. */
17
+ export type RelationKind = "1-to-many" | "many-to-1";
18
+ /** Prisma `onDelete` referential action. */
19
+ export type RelationOnDelete = "Cascade" | "Restrict" | "NoAction" | "SetNull" | "SetDefault";
20
+ /**
21
+ * Relation metadata attached to a {@link FieldDef}.
22
+ *
23
+ * - `1-to-many` fields point at the related model and require no inverse
24
+ * column on this side (`fields`/`references` are inferred by Prisma).
25
+ * - `many-to-1` fields own the foreign key — Prisma needs `fields` and
26
+ * `references` to wire it up.
27
+ */
28
+ export interface RelationDef {
29
+ kind: RelationKind;
30
+ model: string;
31
+ fields?: readonly string[];
32
+ references?: readonly string[];
33
+ onDelete?: RelationOnDelete;
34
+ }
35
+ /**
36
+ * Definition of a single field in a Siteping database model.
37
+ *
38
+ * The interface intentionally keeps a wide structural shape so it stays
39
+ * easy to extend, but consumers can narrow via {@link isRelationField} /
40
+ * {@link isScalarField} when relation vs. scalar logic diverges.
41
+ */
42
+ export interface FieldDef {
43
+ /** Prisma type (e.g. "String", "Int") for scalars, model name for relations. */
44
+ type: PrismaScalarType | (string & {});
45
+ /** Default literal (`"open"`) or function call (`now()`, `cuid()`). */
46
+ default?: string;
47
+ /** Whether the column is nullable. */
48
+ optional?: boolean;
49
+ /** Set on relation fields — absent on scalars. */
50
+ relation?: RelationDef;
51
+ isId?: boolean;
52
+ isUnique?: boolean;
53
+ /** Prisma native type attribute (e.g. "Text" for @db.Text) — used for MySQL compatibility on long strings */
54
+ nativeType?: PrismaNativeType;
55
+ /** Prisma @updatedAt attribute */
56
+ isUpdatedAt?: boolean;
57
+ }
58
+ /** Narrowing predicate: returns `true` when `field` declares a Prisma relation. */
59
+ export declare function isRelationField(field: FieldDef): field is FieldDef & {
60
+ relation: RelationDef;
61
+ };
62
+ /** Narrowing predicate: returns `true` when `field` is a Prisma scalar (no relation metadata). */
63
+ export declare function isScalarField(field: FieldDef): field is FieldDef & {
64
+ relation?: undefined;
65
+ };
66
+ /** Definition of a composite index on a Siteping database model. */
67
+ export interface IndexDef {
68
+ fields: readonly string[];
69
+ }
70
+ /** Definition of a single Siteping database model (fields + indexes). */
71
+ export interface ModelDef {
72
+ fields: Record<string, FieldDef>;
73
+ indexes?: readonly IndexDef[];
74
+ }
75
+ declare const _SITEPING_MODELS: {
76
+ readonly SitepingFeedback: {
77
+ readonly fields: {
78
+ readonly id: {
79
+ readonly type: "String";
80
+ readonly isId: true;
81
+ readonly default: "cuid()";
82
+ };
83
+ readonly projectName: {
84
+ readonly type: "String";
85
+ };
86
+ readonly type: {
87
+ readonly type: "String";
88
+ };
89
+ readonly message: {
90
+ readonly type: "String";
91
+ readonly nativeType: "Text";
92
+ };
93
+ readonly status: {
94
+ readonly type: "String";
95
+ readonly default: "\"open\"";
96
+ };
97
+ readonly url: {
98
+ readonly type: "String";
99
+ };
100
+ readonly urlPattern: {
101
+ readonly type: "String";
102
+ readonly optional: true;
103
+ };
104
+ readonly screenshotUrl: {
105
+ readonly type: "String";
106
+ readonly optional: true;
107
+ readonly nativeType: "Text";
108
+ };
109
+ readonly screenshotRegion: {
110
+ readonly type: "Json";
111
+ readonly optional: true;
112
+ };
113
+ readonly diagnostics: {
114
+ readonly type: "Json";
115
+ readonly optional: true;
116
+ };
117
+ readonly viewport: {
118
+ readonly type: "String";
119
+ };
120
+ readonly userAgent: {
121
+ readonly type: "String";
122
+ };
123
+ readonly authorName: {
124
+ readonly type: "String";
125
+ };
126
+ readonly authorEmail: {
127
+ readonly type: "String";
128
+ };
129
+ readonly clientId: {
130
+ readonly type: "String";
131
+ readonly isUnique: true;
132
+ };
133
+ readonly resolvedAt: {
134
+ readonly type: "DateTime";
135
+ readonly optional: true;
136
+ };
137
+ readonly createdAt: {
138
+ readonly type: "DateTime";
139
+ readonly default: "now()";
140
+ };
141
+ readonly updatedAt: {
142
+ readonly type: "DateTime";
143
+ readonly isUpdatedAt: true;
144
+ };
145
+ readonly annotations: {
146
+ readonly type: "SitepingAnnotation";
147
+ readonly relation: {
148
+ readonly kind: "1-to-many";
149
+ readonly model: "SitepingAnnotation";
150
+ };
151
+ };
152
+ readonly comments: {
153
+ readonly type: "SitepingComment";
154
+ readonly relation: {
155
+ readonly kind: "1-to-many";
156
+ readonly model: "SitepingComment";
157
+ };
158
+ };
159
+ };
160
+ readonly indexes: readonly [{
161
+ readonly fields: readonly ["projectName"];
162
+ }, {
163
+ readonly fields: readonly ["projectName", "status", "createdAt"];
164
+ }, {
165
+ readonly fields: readonly ["projectName", "url"];
166
+ }];
167
+ };
168
+ readonly SitepingAnnotation: {
169
+ readonly fields: {
170
+ readonly id: {
171
+ readonly type: "String";
172
+ readonly isId: true;
173
+ readonly default: "cuid()";
174
+ };
175
+ readonly feedbackId: {
176
+ readonly type: "String";
177
+ };
178
+ readonly feedback: {
179
+ readonly type: "SitepingFeedback";
180
+ readonly relation: {
181
+ readonly kind: "many-to-1";
182
+ readonly model: "SitepingFeedback";
183
+ readonly fields: readonly ["feedbackId"];
184
+ readonly references: readonly ["id"];
185
+ readonly onDelete: "Cascade";
186
+ };
187
+ };
188
+ readonly cssSelector: {
189
+ readonly type: "String";
190
+ readonly nativeType: "Text";
191
+ };
192
+ readonly xpath: {
193
+ readonly type: "String";
194
+ readonly nativeType: "Text";
195
+ };
196
+ readonly textSnippet: {
197
+ readonly type: "String";
198
+ readonly nativeType: "Text";
199
+ };
200
+ readonly elementTag: {
201
+ readonly type: "String";
202
+ };
203
+ readonly elementId: {
204
+ readonly type: "String";
205
+ readonly optional: true;
206
+ };
207
+ readonly textPrefix: {
208
+ readonly type: "String";
209
+ readonly nativeType: "Text";
210
+ };
211
+ readonly textSuffix: {
212
+ readonly type: "String";
213
+ readonly nativeType: "Text";
214
+ };
215
+ readonly fingerprint: {
216
+ readonly type: "String";
217
+ };
218
+ readonly neighborText: {
219
+ readonly type: "String";
220
+ readonly nativeType: "Text";
221
+ };
222
+ readonly anchorKey: {
223
+ readonly type: "String";
224
+ readonly optional: true;
225
+ };
226
+ readonly xPct: {
227
+ readonly type: "Float";
228
+ };
229
+ readonly yPct: {
230
+ readonly type: "Float";
231
+ };
232
+ readonly wPct: {
233
+ readonly type: "Float";
234
+ };
235
+ readonly hPct: {
236
+ readonly type: "Float";
237
+ };
238
+ readonly scrollX: {
239
+ readonly type: "Float";
240
+ };
241
+ readonly scrollY: {
242
+ readonly type: "Float";
243
+ };
244
+ readonly viewportW: {
245
+ readonly type: "Int";
246
+ };
247
+ readonly viewportH: {
248
+ readonly type: "Int";
249
+ };
250
+ readonly devicePixelRatio: {
251
+ readonly type: "Float";
252
+ readonly default: "1";
253
+ };
254
+ readonly createdAt: {
255
+ readonly type: "DateTime";
256
+ readonly default: "now()";
257
+ };
258
+ };
259
+ readonly indexes: readonly [{
260
+ readonly fields: readonly ["feedbackId"];
261
+ }];
262
+ };
263
+ readonly SitepingComment: {
264
+ readonly fields: {
265
+ readonly id: {
266
+ readonly type: "String";
267
+ readonly isId: true;
268
+ readonly default: "cuid()";
269
+ };
270
+ readonly feedbackId: {
271
+ readonly type: "String";
272
+ };
273
+ readonly feedback: {
274
+ readonly type: "SitepingFeedback";
275
+ readonly relation: {
276
+ readonly kind: "many-to-1";
277
+ readonly model: "SitepingFeedback";
278
+ readonly fields: readonly ["feedbackId"];
279
+ readonly references: readonly ["id"];
280
+ readonly onDelete: "Cascade";
281
+ };
282
+ };
283
+ readonly body: {
284
+ readonly type: "String";
285
+ readonly nativeType: "Text";
286
+ };
287
+ readonly authorName: {
288
+ readonly type: "String";
289
+ };
290
+ readonly authorEmail: {
291
+ readonly type: "String";
292
+ };
293
+ readonly authorRole: {
294
+ readonly type: "String";
295
+ readonly default: "\"client\"";
296
+ };
297
+ readonly clientId: {
298
+ readonly type: "String";
299
+ readonly isUnique: true;
300
+ };
301
+ readonly createdAt: {
302
+ readonly type: "DateTime";
303
+ readonly default: "now()";
304
+ };
305
+ };
306
+ readonly indexes: readonly [{
307
+ readonly fields: readonly ["feedbackId", "createdAt"];
308
+ }];
309
+ };
310
+ };
311
+ /** Map of Siteping models keyed by model name — frozen at runtime. */
312
+ export declare const SITEPING_MODELS: typeof _SITEPING_MODELS;
313
+ /** Union of every Siteping model name as a string literal. */
314
+ export type SitepingModelName = keyof typeof SITEPING_MODELS;
315
+ /** Field names declared on a specific Siteping model. */
316
+ export type SitepingModelFieldName<M extends SitepingModelName> = keyof (typeof SITEPING_MODELS)[M]["fields"];
317
+ export {};