@coursebuilder/analytics 1.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.
- package/LICENSE +21 -0
- package/dist/api/index.d.ts +158 -0
- package/dist/api/index.js +317 -0
- package/dist/api/index.js.map +1 -0
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.js +209 -0
- package/dist/catalog.js.map +1 -0
- package/dist/components/index.d.ts +172 -0
- package/dist/components/index.js +1258 -0
- package/dist/components/index.js.map +1 -0
- package/dist/engine.d.ts +20 -0
- package/dist/engine.js +350 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/database.d.ts +79 -0
- package/dist/providers/database.js +533 -0
- package/dist/providers/database.js.map +1 -0
- package/dist/providers/derived.d.ts +45 -0
- package/dist/providers/derived.js +32 -0
- package/dist/providers/derived.js.map +1 -0
- package/dist/providers/ga4.d.ts +43 -0
- package/dist/providers/ga4.js +220 -0
- package/dist/providers/ga4.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +1239 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mux.d.ts +103 -0
- package/dist/providers/mux.js +241 -0
- package/dist/providers/mux.js.map +1 -0
- package/dist/providers/survey.d.ts +102 -0
- package/dist/providers/survey.js +233 -0
- package/dist/providers/survey.js.map +1 -0
- package/dist/types.d.ts +303 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +101 -0
- package/src/api/catalog-handler.ts +321 -0
- package/src/api/index.ts +4 -0
- package/src/api/token-handler.ts +71 -0
- package/src/catalog.ts +223 -0
- package/src/components/country-chart.tsx +114 -0
- package/src/components/index.ts +5 -0
- package/src/components/omnibus-dashboard.tsx +1460 -0
- package/src/components/revenue-chart.tsx +251 -0
- package/src/components/use-chart-colors.ts +75 -0
- package/src/engine.ts +201 -0
- package/src/index.ts +7 -0
- package/src/providers/database.ts +795 -0
- package/src/providers/derived.ts +79 -0
- package/src/providers/ga4.ts +173 -0
- package/src/providers/index.ts +44 -0
- package/src/providers/mux.ts +438 -0
- package/src/providers/survey.ts +487 -0
- package/src/types.ts +333 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { AnalyticsRange } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal column shape required from the contentResource table.
|
|
5
|
+
* We only reference the columns we actually query against.
|
|
6
|
+
*/
|
|
7
|
+
interface ContentResourceTable {
|
|
8
|
+
id: any;
|
|
9
|
+
type: any;
|
|
10
|
+
fields: any;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Minimal column shape required from the contentResourceResource table.
|
|
14
|
+
*/
|
|
15
|
+
interface ContentResourceResourceTable {
|
|
16
|
+
resourceOfId: any;
|
|
17
|
+
resourceId: any;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Minimal column shape required from the questionResponse table.
|
|
21
|
+
*/
|
|
22
|
+
interface QuestionResponseTable {
|
|
23
|
+
id: any;
|
|
24
|
+
surveyId: any;
|
|
25
|
+
questionId: any;
|
|
26
|
+
respondentKey: any;
|
|
27
|
+
surveySessionId: any;
|
|
28
|
+
userId: any;
|
|
29
|
+
emailListSubscriberId: any;
|
|
30
|
+
createdAt: any;
|
|
31
|
+
updatedAt: any;
|
|
32
|
+
fields: any;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Minimal column shape required from the users table.
|
|
36
|
+
* Optional — only needed for getSurveyResponses to resolve user emails.
|
|
37
|
+
*/
|
|
38
|
+
interface UsersTable {
|
|
39
|
+
id: any;
|
|
40
|
+
email: any;
|
|
41
|
+
}
|
|
42
|
+
interface SurveyAnalyticsSchema {
|
|
43
|
+
contentResource: ContentResourceTable;
|
|
44
|
+
contentResourceResource: ContentResourceResourceTable;
|
|
45
|
+
questionResponse: QuestionResponseTable;
|
|
46
|
+
users?: UsersTable;
|
|
47
|
+
}
|
|
48
|
+
interface SurveyAnalyticsProvider {
|
|
49
|
+
getSurveySummary: (range?: AnalyticsRange) => Promise<{
|
|
50
|
+
totalSurveys: number;
|
|
51
|
+
totalResponses: number;
|
|
52
|
+
uniqueRespondents: number;
|
|
53
|
+
avgResponsesPerSurvey: number;
|
|
54
|
+
}>;
|
|
55
|
+
getSurveyList: (range?: AnalyticsRange) => Promise<Array<{
|
|
56
|
+
surveyId: string;
|
|
57
|
+
surveyTitle: string;
|
|
58
|
+
surveySlug: string;
|
|
59
|
+
responses: number;
|
|
60
|
+
uniqueRespondents: number;
|
|
61
|
+
questionCount: number;
|
|
62
|
+
}>>;
|
|
63
|
+
getSurveyResponsesByDay: (range?: AnalyticsRange) => Promise<Array<{
|
|
64
|
+
date: string;
|
|
65
|
+
responses: number;
|
|
66
|
+
}>>;
|
|
67
|
+
getSurveyQuestionBreakdown: (range?: AnalyticsRange, limit?: number) => Promise<Array<{
|
|
68
|
+
questionId: string;
|
|
69
|
+
question: string;
|
|
70
|
+
type: string | null;
|
|
71
|
+
responses: number;
|
|
72
|
+
uniqueRespondents: number;
|
|
73
|
+
answerDistribution: Array<{
|
|
74
|
+
answer: string;
|
|
75
|
+
count: number;
|
|
76
|
+
}>;
|
|
77
|
+
}>>;
|
|
78
|
+
getSurveyResponses: (range?: AnalyticsRange, limit?: number) => Promise<Array<{
|
|
79
|
+
responseId: string;
|
|
80
|
+
surveyId: string;
|
|
81
|
+
surveyTitle: string;
|
|
82
|
+
surveySlug: string;
|
|
83
|
+
questionId: string;
|
|
84
|
+
questionText: string;
|
|
85
|
+
questionType: string | null;
|
|
86
|
+
answer: string;
|
|
87
|
+
userId: string | null;
|
|
88
|
+
userEmail: string | null;
|
|
89
|
+
emailListSubscriberId: string | null;
|
|
90
|
+
createdAt: string;
|
|
91
|
+
}>>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates a survey analytics provider bound to the given Drizzle db instance
|
|
95
|
+
* and schema tables.
|
|
96
|
+
*
|
|
97
|
+
* @param db - Drizzle database instance
|
|
98
|
+
* @param schema - Object containing the required table references
|
|
99
|
+
*/
|
|
100
|
+
declare function createSurveyProvider(db: any, schema: SurveyAnalyticsSchema): SurveyAnalyticsProvider;
|
|
101
|
+
|
|
102
|
+
export { type SurveyAnalyticsProvider, type SurveyAnalyticsSchema, createSurveyProvider };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/providers/survey.ts
|
|
5
|
+
import { and, count, eq, sql } from "drizzle-orm";
|
|
6
|
+
function normalizeRespondentKey(row) {
|
|
7
|
+
if (row.respondentKey)
|
|
8
|
+
return row.respondentKey;
|
|
9
|
+
if (row.userId)
|
|
10
|
+
return `user:${row.userId}`;
|
|
11
|
+
if (row.emailListSubscriberId) {
|
|
12
|
+
return `subscriber:${row.emailListSubscriberId}`;
|
|
13
|
+
}
|
|
14
|
+
if (row.surveySessionId)
|
|
15
|
+
return `session:${row.surveySessionId}`;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
__name(normalizeRespondentKey, "normalizeRespondentKey");
|
|
19
|
+
function getRowTimestamp(row) {
|
|
20
|
+
const date = row.updatedAt ?? row.createdAt;
|
|
21
|
+
return date instanceof Date ? date.getTime() : 0;
|
|
22
|
+
}
|
|
23
|
+
__name(getRowTimestamp, "getRowTimestamp");
|
|
24
|
+
function sortByNewest(rows) {
|
|
25
|
+
return [
|
|
26
|
+
...rows
|
|
27
|
+
].sort((a, b) => getRowTimestamp(b) - getRowTimestamp(a));
|
|
28
|
+
}
|
|
29
|
+
__name(sortByNewest, "sortByNewest");
|
|
30
|
+
function createSurveyProvider(db, schema) {
|
|
31
|
+
const { contentResource, contentResourceResource, questionResponse } = schema;
|
|
32
|
+
function rangeToInterval(range) {
|
|
33
|
+
switch (range) {
|
|
34
|
+
case "24h":
|
|
35
|
+
return "1 DAY";
|
|
36
|
+
case "7d":
|
|
37
|
+
return "7 DAY";
|
|
38
|
+
case "30d":
|
|
39
|
+
return "30 DAY";
|
|
40
|
+
case "90d":
|
|
41
|
+
return "90 DAY";
|
|
42
|
+
case "all":
|
|
43
|
+
return "3650 DAY";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
__name(rangeToInterval, "rangeToInterval");
|
|
47
|
+
function rangeWhere(range, column) {
|
|
48
|
+
return sql`${column} >= DATE_SUB(NOW(), INTERVAL ${sql.raw(rangeToInterval(range))})`;
|
|
49
|
+
}
|
|
50
|
+
__name(rangeWhere, "rangeWhere");
|
|
51
|
+
async function fetchCanonicalRows(range) {
|
|
52
|
+
const { users } = schema;
|
|
53
|
+
const rawRows = users ? await db.select({
|
|
54
|
+
responseId: questionResponse.id,
|
|
55
|
+
surveyId: questionResponse.surveyId,
|
|
56
|
+
surveyTitle: sql`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,
|
|
57
|
+
surveySlug: sql`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,
|
|
58
|
+
questionId: questionResponse.questionId,
|
|
59
|
+
questionText: sql`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,
|
|
60
|
+
questionType: sql`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,
|
|
61
|
+
answer: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
|
|
62
|
+
respondentKey: questionResponse.respondentKey,
|
|
63
|
+
surveySessionId: questionResponse.surveySessionId,
|
|
64
|
+
userId: questionResponse.userId,
|
|
65
|
+
userEmail: users.email,
|
|
66
|
+
emailListSubscriberId: questionResponse.emailListSubscriberId,
|
|
67
|
+
createdAt: questionResponse.createdAt,
|
|
68
|
+
updatedAt: questionResponse.updatedAt
|
|
69
|
+
}).from(questionResponse).leftJoin(sql`${contentResource} AS survey_cr`, sql`survey_cr.id = ${questionResponse.surveyId}`).leftJoin(sql`${contentResource} AS question_cr`, sql`question_cr.id = ${questionResponse.questionId}`).leftJoin(users, eq(questionResponse.userId, users.id)).where(rangeWhere(range, questionResponse.createdAt)) : await db.select({
|
|
70
|
+
responseId: questionResponse.id,
|
|
71
|
+
surveyId: questionResponse.surveyId,
|
|
72
|
+
surveyTitle: sql`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,
|
|
73
|
+
surveySlug: sql`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,
|
|
74
|
+
questionId: questionResponse.questionId,
|
|
75
|
+
questionText: sql`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,
|
|
76
|
+
questionType: sql`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,
|
|
77
|
+
answer: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
|
|
78
|
+
respondentKey: questionResponse.respondentKey,
|
|
79
|
+
surveySessionId: questionResponse.surveySessionId,
|
|
80
|
+
userId: questionResponse.userId,
|
|
81
|
+
userEmail: sql`NULL`,
|
|
82
|
+
emailListSubscriberId: questionResponse.emailListSubscriberId,
|
|
83
|
+
createdAt: questionResponse.createdAt,
|
|
84
|
+
updatedAt: questionResponse.updatedAt
|
|
85
|
+
}).from(questionResponse).leftJoin(sql`${contentResource} AS survey_cr`, sql`survey_cr.id = ${questionResponse.surveyId}`).leftJoin(sql`${contentResource} AS question_cr`, sql`question_cr.id = ${questionResponse.questionId}`).where(rangeWhere(range, questionResponse.createdAt));
|
|
86
|
+
const latestByAnswer = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const row of rawRows) {
|
|
88
|
+
const respondentKey = normalizeRespondentKey(row);
|
|
89
|
+
if (!respondentKey)
|
|
90
|
+
continue;
|
|
91
|
+
const dedupeKey = `${row.surveyId}::${row.questionId}::${respondentKey}`;
|
|
92
|
+
const current = latestByAnswer.get(dedupeKey);
|
|
93
|
+
if (!current || getRowTimestamp(row) >= getRowTimestamp(current)) {
|
|
94
|
+
latestByAnswer.set(dedupeKey, {
|
|
95
|
+
...row,
|
|
96
|
+
respondentKey
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return sortByNewest(Array.from(latestByAnswer.values()));
|
|
101
|
+
}
|
|
102
|
+
__name(fetchCanonicalRows, "fetchCanonicalRows");
|
|
103
|
+
async function getSurveySummary(range = "30d") {
|
|
104
|
+
const [surveyCount] = await db.select({
|
|
105
|
+
total: count()
|
|
106
|
+
}).from(contentResource).where(eq(contentResource.type, "survey"));
|
|
107
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
108
|
+
const respondentKeys = new Set(canonicalRows.map((row) => row.respondentKey));
|
|
109
|
+
const totalSurveys = surveyCount?.total ?? 0;
|
|
110
|
+
const totalResponses = canonicalRows.length;
|
|
111
|
+
return {
|
|
112
|
+
totalSurveys,
|
|
113
|
+
totalResponses,
|
|
114
|
+
uniqueRespondents: respondentKeys.size,
|
|
115
|
+
avgResponsesPerSurvey: totalSurveys > 0 ? totalResponses / totalSurveys : 0
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
__name(getSurveySummary, "getSurveySummary");
|
|
119
|
+
async function getSurveyList(range = "30d") {
|
|
120
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
121
|
+
const responsesBySurvey = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const row of canonicalRows) {
|
|
123
|
+
const current = responsesBySurvey.get(row.surveyId) ?? {
|
|
124
|
+
responses: 0,
|
|
125
|
+
respondents: /* @__PURE__ */ new Set()
|
|
126
|
+
};
|
|
127
|
+
current.responses += 1;
|
|
128
|
+
current.respondents.add(row.respondentKey);
|
|
129
|
+
responsesBySurvey.set(row.surveyId, current);
|
|
130
|
+
}
|
|
131
|
+
const surveys = await db.select({
|
|
132
|
+
surveyId: contentResource.id,
|
|
133
|
+
surveyTitle: sql`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.title'))`,
|
|
134
|
+
surveySlug: sql`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.slug'))`
|
|
135
|
+
}).from(contentResource).where(eq(contentResource.type, "survey"));
|
|
136
|
+
const questionCounts = await db.select({
|
|
137
|
+
surveyId: contentResourceResource.resourceOfId,
|
|
138
|
+
questionCount: count()
|
|
139
|
+
}).from(contentResourceResource).innerJoin(contentResource, and(eq(contentResourceResource.resourceId, contentResource.id), eq(contentResource.type, "question"))).groupBy(contentResourceResource.resourceOfId);
|
|
140
|
+
const questionCountMap = new Map(questionCounts.map((qc) => [
|
|
141
|
+
qc.surveyId,
|
|
142
|
+
qc.questionCount
|
|
143
|
+
]));
|
|
144
|
+
return surveys.map((s) => {
|
|
145
|
+
const counts = responsesBySurvey.get(s.surveyId);
|
|
146
|
+
return {
|
|
147
|
+
surveyId: s.surveyId,
|
|
148
|
+
surveyTitle: s.surveyTitle ?? "",
|
|
149
|
+
surveySlug: s.surveySlug ?? "",
|
|
150
|
+
responses: counts?.responses ?? 0,
|
|
151
|
+
uniqueRespondents: counts?.respondents.size ?? 0,
|
|
152
|
+
questionCount: questionCountMap.get(s.surveyId) ?? 0
|
|
153
|
+
};
|
|
154
|
+
}).sort((a, b) => b.responses - a.responses);
|
|
155
|
+
}
|
|
156
|
+
__name(getSurveyList, "getSurveyList");
|
|
157
|
+
async function getSurveyResponsesByDay(range = "30d") {
|
|
158
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
159
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const row of canonicalRows) {
|
|
161
|
+
if (!(row.createdAt instanceof Date))
|
|
162
|
+
continue;
|
|
163
|
+
const date = row.createdAt.toISOString().slice(0, 10);
|
|
164
|
+
grouped.set(date, (grouped.get(date) ?? 0) + 1);
|
|
165
|
+
}
|
|
166
|
+
return Array.from(grouped.entries()).sort((entryA, entryB) => entryA[0].localeCompare(entryB[0])).map(([date, responses]) => ({
|
|
167
|
+
date,
|
|
168
|
+
responses
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
__name(getSurveyResponsesByDay, "getSurveyResponsesByDay");
|
|
172
|
+
async function getSurveyQuestionBreakdown(range = "30d", limit = 20) {
|
|
173
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
174
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const row of canonicalRows) {
|
|
176
|
+
const current = grouped.get(row.questionId) ?? {
|
|
177
|
+
questionId: row.questionId,
|
|
178
|
+
question: row.questionText ?? "",
|
|
179
|
+
type: row.questionType ?? null,
|
|
180
|
+
responses: 0,
|
|
181
|
+
respondents: /* @__PURE__ */ new Set(),
|
|
182
|
+
answers: /* @__PURE__ */ new Map()
|
|
183
|
+
};
|
|
184
|
+
current.responses += 1;
|
|
185
|
+
current.respondents.add(row.respondentKey);
|
|
186
|
+
const answer = row.answer ?? "(no answer)";
|
|
187
|
+
current.answers.set(answer, (current.answers.get(answer) ?? 0) + 1);
|
|
188
|
+
grouped.set(row.questionId, current);
|
|
189
|
+
}
|
|
190
|
+
return Array.from(grouped.values()).sort((a, b) => b.responses - a.responses).slice(0, limit).map((entry) => ({
|
|
191
|
+
questionId: entry.questionId,
|
|
192
|
+
question: entry.question,
|
|
193
|
+
type: entry.type,
|
|
194
|
+
responses: entry.responses,
|
|
195
|
+
uniqueRespondents: entry.respondents.size,
|
|
196
|
+
answerDistribution: Array.from(entry.answers.entries()).sort((a, b) => b[1] - a[1]).map(([answer, count2]) => ({
|
|
197
|
+
answer,
|
|
198
|
+
count: count2
|
|
199
|
+
}))
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
__name(getSurveyQuestionBreakdown, "getSurveyQuestionBreakdown");
|
|
203
|
+
async function getSurveyResponses(range = "30d", limit = 100) {
|
|
204
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
205
|
+
return canonicalRows.slice(0, limit).map((row) => ({
|
|
206
|
+
responseId: row.responseId,
|
|
207
|
+
surveyId: row.surveyId,
|
|
208
|
+
surveyTitle: row.surveyTitle ?? "",
|
|
209
|
+
surveySlug: row.surveySlug ?? "",
|
|
210
|
+
questionId: row.questionId,
|
|
211
|
+
questionText: row.questionText ?? "",
|
|
212
|
+
questionType: row.questionType ?? null,
|
|
213
|
+
answer: row.answer ?? "",
|
|
214
|
+
userId: row.userId ?? null,
|
|
215
|
+
userEmail: row.userEmail ?? null,
|
|
216
|
+
emailListSubscriberId: row.emailListSubscriberId ?? null,
|
|
217
|
+
createdAt: row.createdAt ? String(row.createdAt) : ""
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
__name(getSurveyResponses, "getSurveyResponses");
|
|
221
|
+
return {
|
|
222
|
+
getSurveySummary,
|
|
223
|
+
getSurveyList,
|
|
224
|
+
getSurveyResponsesByDay,
|
|
225
|
+
getSurveyQuestionBreakdown,
|
|
226
|
+
getSurveyResponses
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
__name(createSurveyProvider, "createSurveyProvider");
|
|
230
|
+
export {
|
|
231
|
+
createSurveyProvider
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=survey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/survey.ts"],"sourcesContent":["import { and, count, eq, sql } from 'drizzle-orm'\n\nimport type { AnalyticsRange } from '../types'\n\n// ─── Schema types ────────────────────────────────────────────────────────────\n\n/**\n * Minimal column shape required from the contentResource table.\n * We only reference the columns we actually query against.\n */\ninterface ContentResourceTable {\n\tid: any\n\ttype: any\n\tfields: any\n}\n\n/**\n * Minimal column shape required from the contentResourceResource table.\n */\ninterface ContentResourceResourceTable {\n\tresourceOfId: any\n\tresourceId: any\n}\n\n/**\n * Minimal column shape required from the questionResponse table.\n */\ninterface QuestionResponseTable {\n\tid: any\n\tsurveyId: any\n\tquestionId: any\n\trespondentKey: any\n\tsurveySessionId: any\n\tuserId: any\n\temailListSubscriberId: any\n\tcreatedAt: any\n\tupdatedAt: any\n\tfields: any\n}\n\n/**\n * Minimal column shape required from the users table.\n * Optional — only needed for getSurveyResponses to resolve user emails.\n */\ninterface UsersTable {\n\tid: any\n\temail: any\n}\n\nexport interface SurveyAnalyticsSchema {\n\tcontentResource: ContentResourceTable\n\tcontentResourceResource: ContentResourceResourceTable\n\tquestionResponse: QuestionResponseTable\n\tusers?: UsersTable\n}\n\ntype CanonicalSurveyRow = {\n\tresponseId: string\n\tsurveyId: string\n\tsurveyTitle: string | null\n\tsurveySlug: string | null\n\tquestionId: string\n\tquestionText: string | null\n\tquestionType: string | null\n\tanswer: string | null\n\trespondentKey: string\n\tsurveySessionId: string | null\n\tuserId: string | null\n\tuserEmail: string | null\n\temailListSubscriberId: string | null\n\tcreatedAt: Date | null\n\tupdatedAt: Date | null\n}\n\n// ─── Return type ─────────────────────────────────────────────────────────────\n\nexport interface SurveyAnalyticsProvider {\n\tgetSurveySummary: (range?: AnalyticsRange) => Promise<{\n\t\ttotalSurveys: number\n\t\ttotalResponses: number\n\t\tuniqueRespondents: number\n\t\tavgResponsesPerSurvey: number\n\t}>\n\n\tgetSurveyList: (range?: AnalyticsRange) => Promise<\n\t\tArray<{\n\t\t\tsurveyId: string\n\t\t\tsurveyTitle: string\n\t\t\tsurveySlug: string\n\t\t\tresponses: number\n\t\t\tuniqueRespondents: number\n\t\t\tquestionCount: number\n\t\t}>\n\t>\n\n\tgetSurveyResponsesByDay: (range?: AnalyticsRange) => Promise<\n\t\tArray<{\n\t\t\tdate: string\n\t\t\tresponses: number\n\t\t}>\n\t>\n\n\tgetSurveyQuestionBreakdown: (\n\t\trange?: AnalyticsRange,\n\t\tlimit?: number,\n\t) => Promise<\n\t\tArray<{\n\t\t\tquestionId: string\n\t\t\tquestion: string\n\t\t\ttype: string | null\n\t\t\tresponses: number\n\t\t\tuniqueRespondents: number\n\t\t\tanswerDistribution: Array<{ answer: string; count: number }>\n\t\t}>\n\t>\n\n\tgetSurveyResponses: (\n\t\trange?: AnalyticsRange,\n\t\tlimit?: number,\n\t) => Promise<\n\t\tArray<{\n\t\t\tresponseId: string\n\t\t\tsurveyId: string\n\t\t\tsurveyTitle: string\n\t\t\tsurveySlug: string\n\t\t\tquestionId: string\n\t\t\tquestionText: string\n\t\t\tquestionType: string | null\n\t\t\tanswer: string\n\t\t\tuserId: string | null\n\t\t\tuserEmail: string | null\n\t\t\temailListSubscriberId: string | null\n\t\t\tcreatedAt: string\n\t\t}>\n\t>\n}\n\nfunction normalizeRespondentKey(row: {\n\trespondentKey: string | null\n\tuserId: string | null\n\temailListSubscriberId: string | null\n\tsurveySessionId: string | null\n}) {\n\tif (row.respondentKey) return row.respondentKey\n\tif (row.userId) return `user:${row.userId}`\n\tif (row.emailListSubscriberId) {\n\t\treturn `subscriber:${row.emailListSubscriberId}`\n\t}\n\tif (row.surveySessionId) return `session:${row.surveySessionId}`\n\treturn null\n}\n\nfunction getRowTimestamp(row: {\n\tupdatedAt: Date | null\n\tcreatedAt: Date | null\n}) {\n\tconst date = row.updatedAt ?? row.createdAt\n\treturn date instanceof Date ? date.getTime() : 0\n}\n\nfunction sortByNewest<\n\tT extends { createdAt: Date | null; updatedAt: Date | null },\n>(rows: T[]) {\n\treturn [...rows].sort((a, b) => getRowTimestamp(b) - getRowTimestamp(a))\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a survey analytics provider bound to the given Drizzle db instance\n * and schema tables.\n *\n * @param db - Drizzle database instance\n * @param schema - Object containing the required table references\n */\nexport function createSurveyProvider(\n\tdb: any,\n\tschema: SurveyAnalyticsSchema,\n): SurveyAnalyticsProvider {\n\tconst { contentResource, contentResourceResource, questionResponse } = schema\n\n\t// ─── Range helpers ───────────────────────────────────────────────────────\n\n\tfunction rangeToInterval(range: AnalyticsRange): string {\n\t\tswitch (range) {\n\t\t\tcase '24h':\n\t\t\t\treturn '1 DAY'\n\t\t\tcase '7d':\n\t\t\t\treturn '7 DAY'\n\t\t\tcase '30d':\n\t\t\t\treturn '30 DAY'\n\t\t\tcase '90d':\n\t\t\t\treturn '90 DAY'\n\t\t\tcase 'all':\n\t\t\t\treturn '3650 DAY'\n\t\t}\n\t}\n\n\tfunction rangeWhere(range: AnalyticsRange, column: any) {\n\t\treturn sql`${column} >= DATE_SUB(NOW(), INTERVAL ${sql.raw(rangeToInterval(range))})`\n\t}\n\n\tasync function fetchCanonicalRows(range: AnalyticsRange) {\n\t\tconst { users } = schema\n\n\t\tconst rawRows = users\n\t\t\t? await db\n\t\t\t\t\t.select({\n\t\t\t\t\t\tresponseId: questionResponse.id,\n\t\t\t\t\t\tsurveyId: questionResponse.surveyId,\n\t\t\t\t\t\tsurveyTitle: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,\n\t\t\t\t\t\tsurveySlug: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,\n\t\t\t\t\t\tquestionId: questionResponse.questionId,\n\t\t\t\t\t\tquestionText: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,\n\t\t\t\t\t\tquestionType: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,\n\t\t\t\t\t\tanswer: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,\n\t\t\t\t\t\trespondentKey: questionResponse.respondentKey,\n\t\t\t\t\t\tsurveySessionId: questionResponse.surveySessionId,\n\t\t\t\t\t\tuserId: questionResponse.userId,\n\t\t\t\t\t\tuserEmail: users.email,\n\t\t\t\t\t\temailListSubscriberId: questionResponse.emailListSubscriberId,\n\t\t\t\t\t\tcreatedAt: questionResponse.createdAt,\n\t\t\t\t\t\tupdatedAt: questionResponse.updatedAt,\n\t\t\t\t\t})\n\t\t\t\t\t.from(questionResponse)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS survey_cr`,\n\t\t\t\t\t\tsql`survey_cr.id = ${questionResponse.surveyId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS question_cr`,\n\t\t\t\t\t\tsql`question_cr.id = ${questionResponse.questionId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.leftJoin(users, eq(questionResponse.userId, users.id))\n\t\t\t\t\t.where(rangeWhere(range, questionResponse.createdAt))\n\t\t\t: await db\n\t\t\t\t\t.select({\n\t\t\t\t\t\tresponseId: questionResponse.id,\n\t\t\t\t\t\tsurveyId: questionResponse.surveyId,\n\t\t\t\t\t\tsurveyTitle: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,\n\t\t\t\t\t\tsurveySlug: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,\n\t\t\t\t\t\tquestionId: questionResponse.questionId,\n\t\t\t\t\t\tquestionText: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,\n\t\t\t\t\t\tquestionType: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,\n\t\t\t\t\t\tanswer: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,\n\t\t\t\t\t\trespondentKey: questionResponse.respondentKey,\n\t\t\t\t\t\tsurveySessionId: questionResponse.surveySessionId,\n\t\t\t\t\t\tuserId: questionResponse.userId,\n\t\t\t\t\t\tuserEmail: sql<string | null>`NULL`,\n\t\t\t\t\t\temailListSubscriberId: questionResponse.emailListSubscriberId,\n\t\t\t\t\t\tcreatedAt: questionResponse.createdAt,\n\t\t\t\t\t\tupdatedAt: questionResponse.updatedAt,\n\t\t\t\t\t})\n\t\t\t\t\t.from(questionResponse)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS survey_cr`,\n\t\t\t\t\t\tsql`survey_cr.id = ${questionResponse.surveyId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS question_cr`,\n\t\t\t\t\t\tsql`question_cr.id = ${questionResponse.questionId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.where(rangeWhere(range, questionResponse.createdAt))\n\n\t\tconst latestByAnswer = new Map<string, CanonicalSurveyRow>()\n\n\t\tfor (const row of rawRows) {\n\t\t\tconst respondentKey = normalizeRespondentKey(row)\n\t\t\tif (!respondentKey) continue\n\n\t\t\tconst dedupeKey = `${row.surveyId}::${row.questionId}::${respondentKey}`\n\t\t\tconst current = latestByAnswer.get(dedupeKey)\n\n\t\t\tif (!current || getRowTimestamp(row) >= getRowTimestamp(current)) {\n\t\t\t\tlatestByAnswer.set(dedupeKey, {\n\t\t\t\t\t...row,\n\t\t\t\t\trespondentKey,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\treturn sortByNewest(Array.from(latestByAnswer.values()))\n\t}\n\n\t// ─── Survey Summary ──────────────────────────────────────────────────────\n\n\tasync function getSurveySummary(range: AnalyticsRange = '30d') {\n\t\tconst [surveyCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(contentResource)\n\t\t\t.where(eq(contentResource.type, 'survey'))\n\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst respondentKeys = new Set(\n\t\t\tcanonicalRows.map((row) => row.respondentKey),\n\t\t)\n\t\tconst totalSurveys = surveyCount?.total ?? 0\n\t\tconst totalResponses = canonicalRows.length\n\n\t\treturn {\n\t\t\ttotalSurveys,\n\t\t\ttotalResponses,\n\t\t\tuniqueRespondents: respondentKeys.size,\n\t\t\tavgResponsesPerSurvey:\n\t\t\t\ttotalSurveys > 0 ? totalResponses / totalSurveys : 0,\n\t\t}\n\t}\n\n\t// ─── Survey List ─────────────────────────────────────────────────────────\n\n\tasync function getSurveyList(range: AnalyticsRange = '30d') {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst responsesBySurvey = new Map<\n\t\t\tstring,\n\t\t\t{ responses: number; respondents: Set<string> }\n\t\t>()\n\n\t\tfor (const row of canonicalRows) {\n\t\t\tconst current = responsesBySurvey.get(row.surveyId) ?? {\n\t\t\t\tresponses: 0,\n\t\t\t\trespondents: new Set<string>(),\n\t\t\t}\n\t\t\tcurrent.responses += 1\n\t\t\tcurrent.respondents.add(row.respondentKey)\n\t\t\tresponsesBySurvey.set(row.surveyId, current)\n\t\t}\n\n\t\tconst surveys = await db\n\t\t\t.select({\n\t\t\t\tsurveyId: contentResource.id,\n\t\t\t\tsurveyTitle: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.title'))`,\n\t\t\t\tsurveySlug: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.slug'))`,\n\t\t\t})\n\t\t\t.from(contentResource)\n\t\t\t.where(eq(contentResource.type, 'survey'))\n\n\t\tconst questionCounts = await db\n\t\t\t.select({\n\t\t\t\tsurveyId: contentResourceResource.resourceOfId,\n\t\t\t\tquestionCount: count(),\n\t\t\t})\n\t\t\t.from(contentResourceResource)\n\t\t\t.innerJoin(\n\t\t\t\tcontentResource,\n\t\t\t\tand(\n\t\t\t\t\teq(contentResourceResource.resourceId, contentResource.id),\n\t\t\t\t\teq(contentResource.type, 'question'),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.groupBy(contentResourceResource.resourceOfId)\n\n\t\tconst questionCountMap = new Map(\n\t\t\tquestionCounts.map((qc: { surveyId: string; questionCount: number }) => [\n\t\t\t\tqc.surveyId,\n\t\t\t\tqc.questionCount,\n\t\t\t]),\n\t\t)\n\n\t\treturn surveys\n\t\t\t.map(\n\t\t\t\t(s: {\n\t\t\t\t\tsurveyId: string\n\t\t\t\t\tsurveyTitle: string | null\n\t\t\t\t\tsurveySlug: string | null\n\t\t\t\t}) => {\n\t\t\t\t\tconst counts = responsesBySurvey.get(s.surveyId)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsurveyId: s.surveyId,\n\t\t\t\t\t\tsurveyTitle: s.surveyTitle ?? '',\n\t\t\t\t\t\tsurveySlug: s.surveySlug ?? '',\n\t\t\t\t\t\tresponses: counts?.responses ?? 0,\n\t\t\t\t\t\tuniqueRespondents: counts?.respondents.size ?? 0,\n\t\t\t\t\t\tquestionCount: questionCountMap.get(s.surveyId) ?? 0,\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t)\n\t\t\t.sort(\n\t\t\t\t(a: { responses: number }, b: { responses: number }) =>\n\t\t\t\t\tb.responses - a.responses,\n\t\t\t)\n\t}\n\n\t// ─── Daily Responses ─────────────────────────────────────────────────────\n\n\tasync function getSurveyResponsesByDay(range: AnalyticsRange = '30d') {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst grouped = new Map<string, number>()\n\n\t\tfor (const row of canonicalRows) {\n\t\t\tif (!(row.createdAt instanceof Date)) continue\n\t\t\tconst date = row.createdAt.toISOString().slice(0, 10)\n\t\t\tgrouped.set(date, (grouped.get(date) ?? 0) + 1)\n\t\t}\n\n\t\treturn Array.from(grouped.entries())\n\t\t\t.sort((entryA: [string, number], entryB: [string, number]) =>\n\t\t\t\tentryA[0].localeCompare(entryB[0]),\n\t\t\t)\n\t\t\t.map(([date, responses]) => ({ date, responses }))\n\t}\n\n\t// ─── Question Breakdown ──────────────────────────────────────────────────\n\n\tasync function getSurveyQuestionBreakdown(\n\t\trange: AnalyticsRange = '30d',\n\t\tlimit = 20,\n\t) {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst grouped = new Map<\n\t\t\tstring,\n\t\t\t{\n\t\t\t\tquestionId: string\n\t\t\t\tquestion: string\n\t\t\t\ttype: string | null\n\t\t\t\tresponses: number\n\t\t\t\trespondents: Set<string>\n\t\t\t\tanswers: Map<string, number>\n\t\t\t}\n\t\t>()\n\n\t\tfor (const row of canonicalRows) {\n\t\t\tconst current = grouped.get(row.questionId) ?? {\n\t\t\t\tquestionId: row.questionId,\n\t\t\t\tquestion: row.questionText ?? '',\n\t\t\t\ttype: row.questionType ?? null,\n\t\t\t\tresponses: 0,\n\t\t\t\trespondents: new Set<string>(),\n\t\t\t\tanswers: new Map<string, number>(),\n\t\t\t}\n\n\t\t\tcurrent.responses += 1\n\t\t\tcurrent.respondents.add(row.respondentKey)\n\t\t\tconst answer = row.answer ?? '(no answer)'\n\t\t\tcurrent.answers.set(answer, (current.answers.get(answer) ?? 0) + 1)\n\t\t\tgrouped.set(row.questionId, current)\n\t\t}\n\n\t\treturn Array.from(grouped.values())\n\t\t\t.sort(\n\t\t\t\t(a: { responses: number }, b: { responses: number }) =>\n\t\t\t\t\tb.responses - a.responses,\n\t\t\t)\n\t\t\t.slice(0, limit)\n\t\t\t.map((entry) => ({\n\t\t\t\tquestionId: entry.questionId,\n\t\t\t\tquestion: entry.question,\n\t\t\t\ttype: entry.type,\n\t\t\t\tresponses: entry.responses,\n\t\t\t\tuniqueRespondents: entry.respondents.size,\n\t\t\t\tanswerDistribution: Array.from(entry.answers.entries())\n\t\t\t\t\t.sort((a: [string, number], b: [string, number]) => b[1] - a[1])\n\t\t\t\t\t.map(([answer, count]) => ({ answer, count })),\n\t\t\t}))\n\t}\n\n\t// ─── Individual Response Rows ────────────────────────────────────────────\n\n\tasync function getSurveyResponses(\n\t\trange: AnalyticsRange = '30d',\n\t\tlimit = 100,\n\t) {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\n\t\treturn canonicalRows.slice(0, limit).map((row) => ({\n\t\t\tresponseId: row.responseId,\n\t\t\tsurveyId: row.surveyId,\n\t\t\tsurveyTitle: row.surveyTitle ?? '',\n\t\t\tsurveySlug: row.surveySlug ?? '',\n\t\t\tquestionId: row.questionId,\n\t\t\tquestionText: row.questionText ?? '',\n\t\t\tquestionType: row.questionType ?? null,\n\t\t\tanswer: row.answer ?? '',\n\t\t\tuserId: row.userId ?? null,\n\t\t\tuserEmail: row.userEmail ?? null,\n\t\t\temailListSubscriberId: row.emailListSubscriberId ?? null,\n\t\t\tcreatedAt: row.createdAt ? String(row.createdAt) : '',\n\t\t}))\n\t}\n\n\treturn {\n\t\tgetSurveySummary,\n\t\tgetSurveyList,\n\t\tgetSurveyResponsesByDay,\n\t\tgetSurveyQuestionBreakdown,\n\t\tgetSurveyResponses,\n\t}\n}\n"],"mappings":";;;;AAAA,SAASA,KAAKC,OAAOC,IAAIC,WAAW;AAyIpC,SAASC,uBAAuBC,KAK/B;AACA,MAAIA,IAAIC;AAAe,WAAOD,IAAIC;AAClC,MAAID,IAAIE;AAAQ,WAAO,QAAQF,IAAIE,MAAM;AACzC,MAAIF,IAAIG,uBAAuB;AAC9B,WAAO,cAAcH,IAAIG,qBAAqB;EAC/C;AACA,MAAIH,IAAII;AAAiB,WAAO,WAAWJ,IAAII,eAAe;AAC9D,SAAO;AACR;AAbSL;AAeT,SAASM,gBAAgBL,KAGxB;AACA,QAAMM,OAAON,IAAIO,aAAaP,IAAIQ;AAClC,SAAOF,gBAAgBG,OAAOH,KAAKI,QAAO,IAAK;AAChD;AANSL;AAQT,SAASM,aAEPC,MAAS;AACV,SAAO;OAAIA;IAAMC,KAAK,CAACC,GAAGC,MAAMV,gBAAgBU,CAAAA,IAAKV,gBAAgBS,CAAAA,CAAAA;AACtE;AAJSH;AAeF,SAASK,qBACfC,IACAC,QAA6B;AAE7B,QAAM,EAAEC,iBAAiBC,yBAAyBC,iBAAgB,IAAKH;AAIvE,WAASI,gBAAgBC,OAAqB;AAC7C,YAAQA,OAAAA;MACP,KAAK;AACJ,eAAO;MACR,KAAK;AACJ,eAAO;MACR,KAAK;AACJ,eAAO;MACR,KAAK;AACJ,eAAO;MACR,KAAK;AACJ,eAAO;IACT;EACD;AAbSD;AAeT,WAASE,WAAWD,OAAuBE,QAAW;AACrD,WAAOC,MAAMD,MAAAA,gCAAsCC,IAAIC,IAAIL,gBAAgBC,KAAAA,CAAAA,CAAAA;EAC5E;AAFSC;AAIT,iBAAeI,mBAAmBL,OAAqB;AACtD,UAAM,EAAEM,MAAK,IAAKX;AAElB,UAAMY,UAAUD,QACb,MAAMZ,GACLc,OAAO;MACPC,YAAYX,iBAAiBY;MAC7BC,UAAUb,iBAAiBa;MAC3BC,aAAaT;MACbU,YAAYV;MACZW,YAAYhB,iBAAiBgB;MAC7BC,cAAcZ;MACda,cAAcb;MACdc,QAAQd,gCAAwCL,iBAAiBoB,MAAM;MACvExC,eAAeoB,iBAAiBpB;MAChCG,iBAAiBiB,iBAAiBjB;MAClCF,QAAQmB,iBAAiBnB;MACzBwC,WAAWb,MAAMc;MACjBxC,uBAAuBkB,iBAAiBlB;MACxCK,WAAWa,iBAAiBb;MAC5BD,WAAWc,iBAAiBd;IAC7B,CAAA,EACCqC,KAAKvB,gBAAAA,EACLwB,SACAnB,MAAMP,eAAAA,iBACNO,qBAAqBL,iBAAiBa,QAAQ,EAAE,EAEhDW,SACAnB,MAAMP,eAAAA,mBACNO,uBAAuBL,iBAAiBgB,UAAU,EAAE,EAEpDQ,SAAShB,OAAOiB,GAAGzB,iBAAiBnB,QAAQ2B,MAAMI,EAAE,CAAA,EACpDc,MAAMvB,WAAWD,OAAOF,iBAAiBb,SAAS,CAAA,IACnD,MAAMS,GACLc,OAAO;MACPC,YAAYX,iBAAiBY;MAC7BC,UAAUb,iBAAiBa;MAC3BC,aAAaT;MACbU,YAAYV;MACZW,YAAYhB,iBAAiBgB;MAC7BC,cAAcZ;MACda,cAAcb;MACdc,QAAQd,gCAAwCL,iBAAiBoB,MAAM;MACvExC,eAAeoB,iBAAiBpB;MAChCG,iBAAiBiB,iBAAiBjB;MAClCF,QAAQmB,iBAAiBnB;MACzBwC,WAAWhB;MACXvB,uBAAuBkB,iBAAiBlB;MACxCK,WAAWa,iBAAiBb;MAC5BD,WAAWc,iBAAiBd;IAC7B,CAAA,EACCqC,KAAKvB,gBAAAA,EACLwB,SACAnB,MAAMP,eAAAA,iBACNO,qBAAqBL,iBAAiBa,QAAQ,EAAE,EAEhDW,SACAnB,MAAMP,eAAAA,mBACNO,uBAAuBL,iBAAiBgB,UAAU,EAAE,EAEpDU,MAAMvB,WAAWD,OAAOF,iBAAiBb,SAAS,CAAA;AAEtD,UAAMwC,iBAAiB,oBAAIC,IAAAA;AAE3B,eAAWjD,OAAO8B,SAAS;AAC1B,YAAM7B,gBAAgBF,uBAAuBC,GAAAA;AAC7C,UAAI,CAACC;AAAe;AAEpB,YAAMiD,YAAY,GAAGlD,IAAIkC,QAAQ,KAAKlC,IAAIqC,UAAU,KAAKpC,aAAAA;AACzD,YAAMkD,UAAUH,eAAeI,IAAIF,SAAAA;AAEnC,UAAI,CAACC,WAAW9C,gBAAgBL,GAAAA,KAAQK,gBAAgB8C,OAAAA,GAAU;AACjEH,uBAAeK,IAAIH,WAAW;UAC7B,GAAGlD;UACHC;QACD,CAAA;MACD;IACD;AAEA,WAAOU,aAAa2C,MAAMV,KAAKI,eAAeO,OAAM,CAAA,CAAA;EACrD;AAhFe3B;AAoFf,iBAAe4B,iBAAiBjC,QAAwB,OAAK;AAC5D,UAAM,CAACkC,WAAAA,IAAe,MAAMxC,GAC1Bc,OAAO;MAAE2B,OAAOC,MAAAA;IAAQ,CAAA,EACxBf,KAAKzB,eAAAA,EACL4B,MAAMD,GAAG3B,gBAAgByC,MAAM,QAAA,CAAA;AAEjC,UAAMC,gBAAgB,MAAMjC,mBAAmBL,KAAAA;AAC/C,UAAMuC,iBAAiB,IAAIC,IAC1BF,cAAcG,IAAI,CAAChE,QAAQA,IAAIC,aAAa,CAAA;AAE7C,UAAMgE,eAAeR,aAAaC,SAAS;AAC3C,UAAMQ,iBAAiBL,cAAcM;AAErC,WAAO;MACNF;MACAC;MACAE,mBAAmBN,eAAeO;MAClCC,uBACCL,eAAe,IAAIC,iBAAiBD,eAAe;IACrD;EACD;AApBeT;AAwBf,iBAAee,cAAchD,QAAwB,OAAK;AACzD,UAAMsC,gBAAgB,MAAMjC,mBAAmBL,KAAAA;AAC/C,UAAMiD,oBAAoB,oBAAIvB,IAAAA;AAK9B,eAAWjD,OAAO6D,eAAe;AAChC,YAAMV,UAAUqB,kBAAkBpB,IAAIpD,IAAIkC,QAAQ,KAAK;QACtDuC,WAAW;QACXC,aAAa,oBAAIX,IAAAA;MAClB;AACAZ,cAAQsB,aAAa;AACrBtB,cAAQuB,YAAYC,IAAI3E,IAAIC,aAAa;AACzCuE,wBAAkBnB,IAAIrD,IAAIkC,UAAUiB,OAAAA;IACrC;AAEA,UAAMyB,UAAU,MAAM3D,GACpBc,OAAO;MACPG,UAAUf,gBAAgBc;MAC1BE,aAAaT,gCAAwCP,gBAAgBsB,MAAM;MAC3EL,YAAYV,gCAAwCP,gBAAgBsB,MAAM;IAC3E,CAAA,EACCG,KAAKzB,eAAAA,EACL4B,MAAMD,GAAG3B,gBAAgByC,MAAM,QAAA,CAAA;AAEjC,UAAMiB,iBAAiB,MAAM5D,GAC3Bc,OAAO;MACPG,UAAUd,wBAAwB0D;MAClCC,eAAepB,MAAAA;IAChB,CAAA,EACCf,KAAKxB,uBAAAA,EACL4D,UACA7D,iBACA8D,IACCnC,GAAG1B,wBAAwB8D,YAAY/D,gBAAgBc,EAAE,GACzDa,GAAG3B,gBAAgByC,MAAM,UAAA,CAAA,CAAA,EAG1BuB,QAAQ/D,wBAAwB0D,YAAY;AAE9C,UAAMM,mBAAmB,IAAInC,IAC5B4B,eAAeb,IAAI,CAACqB,OAAoD;MACvEA,GAAGnD;MACHmD,GAAGN;KACH,CAAA;AAGF,WAAOH,QACLZ,IACA,CAACsB,MAAAA;AAKA,YAAMC,SAASf,kBAAkBpB,IAAIkC,EAAEpD,QAAQ;AAC/C,aAAO;QACNA,UAAUoD,EAAEpD;QACZC,aAAamD,EAAEnD,eAAe;QAC9BC,YAAYkD,EAAElD,cAAc;QAC5BqC,WAAWc,QAAQd,aAAa;QAChCL,mBAAmBmB,QAAQb,YAAYL,QAAQ;QAC/CU,eAAeK,iBAAiBhC,IAAIkC,EAAEpD,QAAQ,KAAK;MACpD;IACD,CAAA,EAEArB,KACA,CAACC,GAA0BC,MAC1BA,EAAE0D,YAAY3D,EAAE2D,SAAS;EAE7B;AAtEeF;AA0Ef,iBAAeiB,wBAAwBjE,QAAwB,OAAK;AACnE,UAAMsC,gBAAgB,MAAMjC,mBAAmBL,KAAAA;AAC/C,UAAMkE,UAAU,oBAAIxC,IAAAA;AAEpB,eAAWjD,OAAO6D,eAAe;AAChC,UAAI,EAAE7D,IAAIQ,qBAAqBC;AAAO;AACtC,YAAMH,OAAON,IAAIQ,UAAUkF,YAAW,EAAGC,MAAM,GAAG,EAAA;AAClDF,cAAQpC,IAAI/C,OAAOmF,QAAQrC,IAAI9C,IAAAA,KAAS,KAAK,CAAA;IAC9C;AAEA,WAAOgD,MAAMV,KAAK6C,QAAQG,QAAO,CAAA,EAC/B/E,KAAK,CAACgF,QAA0BC,WAChCD,OAAO,CAAA,EAAGE,cAAcD,OAAO,CAAA,CAAE,CAAA,EAEjC9B,IAAI,CAAC,CAAC1D,MAAMmE,SAAAA,OAAgB;MAAEnE;MAAMmE;IAAU,EAAA;EACjD;AAfee;AAmBf,iBAAeQ,2BACdzE,QAAwB,OACxB0E,QAAQ,IAAE;AAEV,UAAMpC,gBAAgB,MAAMjC,mBAAmBL,KAAAA;AAC/C,UAAMkE,UAAU,oBAAIxC,IAAAA;AAYpB,eAAWjD,OAAO6D,eAAe;AAChC,YAAMV,UAAUsC,QAAQrC,IAAIpD,IAAIqC,UAAU,KAAK;QAC9CA,YAAYrC,IAAIqC;QAChB6D,UAAUlG,IAAIsC,gBAAgB;QAC9BsB,MAAM5D,IAAIuC,gBAAgB;QAC1BkC,WAAW;QACXC,aAAa,oBAAIX,IAAAA;QACjBoC,SAAS,oBAAIlD,IAAAA;MACd;AAEAE,cAAQsB,aAAa;AACrBtB,cAAQuB,YAAYC,IAAI3E,IAAIC,aAAa;AACzC,YAAMuC,SAASxC,IAAIwC,UAAU;AAC7BW,cAAQgD,QAAQ9C,IAAIb,SAASW,QAAQgD,QAAQ/C,IAAIZ,MAAAA,KAAW,KAAK,CAAA;AACjEiD,cAAQpC,IAAIrD,IAAIqC,YAAYc,OAAAA;IAC7B;AAEA,WAAOG,MAAMV,KAAK6C,QAAQlC,OAAM,CAAA,EAC9B1C,KACA,CAACC,GAA0BC,MAC1BA,EAAE0D,YAAY3D,EAAE2D,SAAS,EAE1BkB,MAAM,GAAGM,KAAAA,EACTjC,IAAI,CAACoC,WAAW;MAChB/D,YAAY+D,MAAM/D;MAClB6D,UAAUE,MAAMF;MAChBtC,MAAMwC,MAAMxC;MACZa,WAAW2B,MAAM3B;MACjBL,mBAAmBgC,MAAM1B,YAAYL;MACrCgC,oBAAoB/C,MAAMV,KAAKwD,MAAMD,QAAQP,QAAO,CAAA,EAClD/E,KAAK,CAACC,GAAqBC,MAAwBA,EAAE,CAAA,IAAKD,EAAE,CAAA,CAAE,EAC9DkD,IAAI,CAAC,CAACxB,QAAQmB,MAAAA,OAAY;QAAEnB;QAAQmB,OAAAA;MAAM,EAAA;IAC7C,EAAA;EACF;AAlDeqC;AAsDf,iBAAeM,mBACd/E,QAAwB,OACxB0E,QAAQ,KAAG;AAEX,UAAMpC,gBAAgB,MAAMjC,mBAAmBL,KAAAA;AAE/C,WAAOsC,cAAc8B,MAAM,GAAGM,KAAAA,EAAOjC,IAAI,CAAChE,SAAS;MAClDgC,YAAYhC,IAAIgC;MAChBE,UAAUlC,IAAIkC;MACdC,aAAanC,IAAImC,eAAe;MAChCC,YAAYpC,IAAIoC,cAAc;MAC9BC,YAAYrC,IAAIqC;MAChBC,cAActC,IAAIsC,gBAAgB;MAClCC,cAAcvC,IAAIuC,gBAAgB;MAClCC,QAAQxC,IAAIwC,UAAU;MACtBtC,QAAQF,IAAIE,UAAU;MACtBwC,WAAW1C,IAAI0C,aAAa;MAC5BvC,uBAAuBH,IAAIG,yBAAyB;MACpDK,WAAWR,IAAIQ,YAAY+F,OAAOvG,IAAIQ,SAAS,IAAI;IACpD,EAAA;EACD;AApBe8F;AAsBf,SAAO;IACN9C;IACAe;IACAiB;IACAQ;IACAM;EACD;AACD;AAvTgBtF;","names":["and","count","eq","sql","normalizeRespondentKey","row","respondentKey","userId","emailListSubscriberId","surveySessionId","getRowTimestamp","date","updatedAt","createdAt","Date","getTime","sortByNewest","rows","sort","a","b","createSurveyProvider","db","schema","contentResource","contentResourceResource","questionResponse","rangeToInterval","range","rangeWhere","column","sql","raw","fetchCanonicalRows","users","rawRows","select","responseId","id","surveyId","surveyTitle","surveySlug","questionId","questionText","questionType","answer","fields","userEmail","email","from","leftJoin","eq","where","latestByAnswer","Map","dedupeKey","current","get","set","Array","values","getSurveySummary","surveyCount","total","count","type","canonicalRows","respondentKeys","Set","map","totalSurveys","totalResponses","length","uniqueRespondents","size","avgResponsesPerSurvey","getSurveyList","responsesBySurvey","responses","respondents","add","surveys","questionCounts","resourceOfId","questionCount","innerJoin","and","resourceId","groupBy","questionCountMap","qc","s","counts","getSurveyResponsesByDay","grouped","toISOString","slice","entries","entryA","entryB","localeCompare","getSurveyQuestionBreakdown","limit","question","answers","entry","answerDistribution","getSurveyResponses","String"]}
|