thryve-mcp-server 0.0.1
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/Overview.csv +351 -0
- package/README.md +229 -0
- package/dist/category-mappings.d.ts +34 -0
- package/dist/category-mappings.d.ts.map +1 -0
- package/dist/category-mappings.js +146 -0
- package/dist/category-mappings.js.map +1 -0
- package/dist/data-enrichment.d.ts +19 -0
- package/dist/data-enrichment.d.ts.map +1 -0
- package/dist/data-enrichment.js +105 -0
- package/dist/data-enrichment.js.map +1 -0
- package/dist/data-types-generated.d.ts +70 -0
- package/dist/data-types-generated.d.ts.map +1 -0
- package/dist/data-types-generated.js +2597 -0
- package/dist/data-types-generated.js.map +1 -0
- package/dist/data-types.d.ts +53 -0
- package/dist/data-types.d.ts.map +1 -0
- package/dist/data-types.js +138 -0
- package/dist/data-types.js.map +1 -0
- package/dist/debug-logger.d.ts +17 -0
- package/dist/debug-logger.d.ts.map +1 -0
- package/dist/debug-logger.js +35 -0
- package/dist/debug-logger.js.map +1 -0
- package/dist/debug-middleware.d.ts +12 -0
- package/dist/debug-middleware.d.ts.map +1 -0
- package/dist/debug-middleware.js +36 -0
- package/dist/debug-middleware.js.map +1 -0
- package/dist/helpers.d.ts +19 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +166 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +754 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas.d.ts +557 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +338 -0
- package/dist/schemas.js.map +1 -0
- package/dist/tool-loader.d.ts +34 -0
- package/dist/tool-loader.d.ts.map +1 -0
- package/dist/tool-loader.js +108 -0
- package/dist/tool-loader.js.map +1 -0
- package/dist/tool-registry.d.ts +44 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +54 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tools/baseCategoryHandler.d.ts +28 -0
- package/dist/tools/baseCategoryHandler.d.ts.map +1 -0
- package/dist/tools/baseCategoryHandler.js +184 -0
- package/dist/tools/baseCategoryHandler.js.map +1 -0
- package/dist/tools/categories/getActivity.d.ts +10 -0
- package/dist/tools/categories/getActivity.d.ts.map +1 -0
- package/dist/tools/categories/getActivity.js +5 -0
- package/dist/tools/categories/getActivity.js.map +1 -0
- package/dist/tools/categories/getAudioAndHearing.d.ts +10 -0
- package/dist/tools/categories/getAudioAndHearing.d.ts.map +1 -0
- package/dist/tools/categories/getAudioAndHearing.js +5 -0
- package/dist/tools/categories/getAudioAndHearing.js.map +1 -0
- package/dist/tools/categories/getBloodGlucose.d.ts +10 -0
- package/dist/tools/categories/getBloodGlucose.d.ts.map +1 -0
- package/dist/tools/categories/getBloodGlucose.js +5 -0
- package/dist/tools/categories/getBloodGlucose.js.map +1 -0
- package/dist/tools/categories/getBodyComposition.d.ts +10 -0
- package/dist/tools/categories/getBodyComposition.d.ts.map +1 -0
- package/dist/tools/categories/getBodyComposition.js +5 -0
- package/dist/tools/categories/getBodyComposition.js.map +1 -0
- package/dist/tools/categories/getCardiovascular.d.ts +10 -0
- package/dist/tools/categories/getCardiovascular.d.ts.map +1 -0
- package/dist/tools/categories/getCardiovascular.js +5 -0
- package/dist/tools/categories/getCardiovascular.js.map +1 -0
- package/dist/tools/categories/getHeartRate.d.ts +10 -0
- package/dist/tools/categories/getHeartRate.d.ts.map +1 -0
- package/dist/tools/categories/getHeartRate.js +5 -0
- package/dist/tools/categories/getHeartRate.js.map +1 -0
- package/dist/tools/categories/getLocation.d.ts +10 -0
- package/dist/tools/categories/getLocation.d.ts.map +1 -0
- package/dist/tools/categories/getLocation.js +5 -0
- package/dist/tools/categories/getLocation.js.map +1 -0
- package/dist/tools/categories/getMicrobiome.d.ts +10 -0
- package/dist/tools/categories/getMicrobiome.d.ts.map +1 -0
- package/dist/tools/categories/getMicrobiome.js +5 -0
- package/dist/tools/categories/getMicrobiome.js.map +1 -0
- package/dist/tools/categories/getMovementAnalysis.d.ts +10 -0
- package/dist/tools/categories/getMovementAnalysis.d.ts.map +1 -0
- package/dist/tools/categories/getMovementAnalysis.js +5 -0
- package/dist/tools/categories/getMovementAnalysis.js.map +1 -0
- package/dist/tools/categories/getNutrition.d.ts +10 -0
- package/dist/tools/categories/getNutrition.d.ts.map +1 -0
- package/dist/tools/categories/getNutrition.js +5 -0
- package/dist/tools/categories/getNutrition.js.map +1 -0
- package/dist/tools/categories/getRespiratory.d.ts +10 -0
- package/dist/tools/categories/getRespiratory.d.ts.map +1 -0
- package/dist/tools/categories/getRespiratory.js +5 -0
- package/dist/tools/categories/getRespiratory.js.map +1 -0
- package/dist/tools/categories/getSleep.d.ts +10 -0
- package/dist/tools/categories/getSleep.d.ts.map +1 -0
- package/dist/tools/categories/getSleep.js +5 -0
- package/dist/tools/categories/getSleep.js.map +1 -0
- package/dist/tools/categories/getStressAndHRV.d.ts +10 -0
- package/dist/tools/categories/getStressAndHRV.d.ts.map +1 -0
- package/dist/tools/categories/getStressAndHRV.js +5 -0
- package/dist/tools/categories/getStressAndHRV.js.map +1 -0
- package/dist/tools/categories/getWellness.d.ts +10 -0
- package/dist/tools/categories/getWellness.d.ts.map +1 -0
- package/dist/tools/categories/getWellness.js +5 -0
- package/dist/tools/categories/getWellness.js.map +1 -0
- package/dist/tools/categories/getWomensHealth.d.ts +10 -0
- package/dist/tools/categories/getWomensHealth.d.ts.map +1 -0
- package/dist/tools/categories/getWomensHealth.js +5 -0
- package/dist/tools/categories/getWomensHealth.js.map +1 -0
- package/dist/tools/categories/getWorkouts.d.ts +10 -0
- package/dist/tools/categories/getWorkouts.d.ts.map +1 -0
- package/dist/tools/categories/getWorkouts.js +5 -0
- package/dist/tools/categories/getWorkouts.js.map +1 -0
- package/dist/tools/categoryTools.d.ts +100 -0
- package/dist/tools/categoryTools.d.ts.map +1 -0
- package/dist/tools/categoryTools.js +66 -0
- package/dist/tools/categoryTools.js.map +1 -0
- package/dist/tools/get.d.ts +39 -0
- package/dist/tools/get.d.ts.map +1 -0
- package/dist/tools/get.js +118 -0
- package/dist/tools/get.js.map +1 -0
- package/dist/tools/getAvailableDataTypes.d.ts +101 -0
- package/dist/tools/getAvailableDataTypes.d.ts.map +1 -0
- package/dist/tools/getAvailableDataTypes.js +120 -0
- package/dist/tools/getAvailableDataTypes.js.map +1 -0
- package/dist/tools/getConnectionWidgetUrl.d.ts +12 -0
- package/dist/tools/getConnectionWidgetUrl.d.ts.map +1 -0
- package/dist/tools/getConnectionWidgetUrl.js +26 -0
- package/dist/tools/getConnectionWidgetUrl.js.map +1 -0
- package/dist/tools/getDailyData.d.ts +30 -0
- package/dist/tools/getDailyData.d.ts.map +1 -0
- package/dist/tools/getDailyData.js +114 -0
- package/dist/tools/getDailyData.js.map +1 -0
- package/dist/tools/getEpochData.d.ts +30 -0
- package/dist/tools/getEpochData.d.ts.map +1 -0
- package/dist/tools/getEpochData.js +111 -0
- package/dist/tools/getEpochData.js.map +1 -0
- package/dist/tools/getHealthDataByCategory.d.ts +38 -0
- package/dist/tools/getHealthDataByCategory.d.ts.map +1 -0
- package/dist/tools/getHealthDataByCategory.js +231 -0
- package/dist/tools/getHealthDataByCategory.js.map +1 -0
- package/dist/tools/getUserInformation.d.ts +8 -0
- package/dist/tools/getUserInformation.d.ts.map +1 -0
- package/dist/tools/getUserInformation.js +24 -0
- package/dist/tools/getUserInformation.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/legacy.d.ts +37 -0
- package/dist/tools/legacy.d.ts.map +1 -0
- package/dist/tools/legacy.js +105 -0
- package/dist/tools/legacy.js.map +1 -0
- package/dist/tools/listConnectedSources.d.ts +11 -0
- package/dist/tools/listConnectedSources.d.ts.map +1 -0
- package/dist/tools/listConnectedSources.js +29 -0
- package/dist/tools/listConnectedSources.js.map +1 -0
- package/dist/tools/listDevices.d.ts +11 -0
- package/dist/tools/listDevices.d.ts.map +1 -0
- package/dist/tools/listDevices.js +29 -0
- package/dist/tools/listDevices.js.map +1 -0
- package/dist/tools/search-filters.d.ts +42 -0
- package/dist/tools/search-filters.d.ts.map +1 -0
- package/dist/tools/search-filters.js +210 -0
- package/dist/tools/search-filters.js.map +1 -0
- package/dist/tools/search-keyword.d.ts +32 -0
- package/dist/tools/search-keyword.d.ts.map +1 -0
- package/dist/tools/search-keyword.js +122 -0
- package/dist/tools/search-keyword.js.map +1 -0
- package/dist/tools/search-tools.d.ts +8 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +273 -0
- package/dist/tools/search-tools.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
- package/parse-csv.py +152 -0
- package/src/category-mappings.ts +181 -0
- package/src/data-enrichment.ts +125 -0
- package/src/data-types-generated.ts +2652 -0
- package/src/helpers.ts +198 -0
- package/src/index.ts +859 -0
- package/src/schemas.ts +372 -0
- package/src/tools/baseCategoryHandler.ts +243 -0
- package/src/tools/categoryTools.ts +101 -0
- package/src/tools/get.ts +147 -0
- package/src/tools/getAvailableDataTypes.ts +148 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/listConnectedSources.ts +45 -0
- package/src/tools/listDevices.ts +45 -0
- package/src/tools/search-filters.ts +253 -0
- package/src/tools/search-keyword.ts +162 -0
- package/src/types.ts +44 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { Request } from "express";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { SearchFiltersSchema } from "../schemas.js";
|
|
4
|
+
import type { HealthDataRecord } from "../types.js";
|
|
5
|
+
import {
|
|
6
|
+
extractThryveHeaders,
|
|
7
|
+
buildUrlEncodedBody,
|
|
8
|
+
getDefaultDateRange,
|
|
9
|
+
getDefaultTimestampRange,
|
|
10
|
+
THRYVE_API_BASE_URL,
|
|
11
|
+
sortHealthData,
|
|
12
|
+
paginateResults,
|
|
13
|
+
} from "../helpers.js";
|
|
14
|
+
import { getDataTypeIdsForCategories, CATEGORY_MAPPINGS, type HealthCategory } from "../category-mappings.js";
|
|
15
|
+
import { flattenThryveResponse, enrichWithDataTypes } from "../data-enrichment.js";
|
|
16
|
+
|
|
17
|
+
export async function searchFilters(req: Request, args: z.infer<typeof SearchFiltersSchema>) {
|
|
18
|
+
const { headers, authToken } = extractThryveHeaders(req);
|
|
19
|
+
|
|
20
|
+
// Determine date/timestamp ranges
|
|
21
|
+
let startDay: string;
|
|
22
|
+
let endDay: string;
|
|
23
|
+
let startTimestamp: string;
|
|
24
|
+
let endTimestamp: string;
|
|
25
|
+
|
|
26
|
+
// Prefer explicit timestamps if provided, otherwise use dates
|
|
27
|
+
if (args.startTimestamp || args.endTimestamp) {
|
|
28
|
+
const defaults = getDefaultTimestampRange();
|
|
29
|
+
startTimestamp = args.startTimestamp || defaults.startTimestamp;
|
|
30
|
+
endTimestamp = args.endTimestamp || defaults.endTimestamp;
|
|
31
|
+
// Extract dates from timestamps
|
|
32
|
+
startDay = startTimestamp.split('T')[0];
|
|
33
|
+
endDay = endTimestamp.split('T')[0];
|
|
34
|
+
} else if (args.startDate || args.endDate) {
|
|
35
|
+
const defaults = getDefaultDateRange();
|
|
36
|
+
startDay = args.startDate || defaults.startDay;
|
|
37
|
+
endDay = args.endDate || defaults.endDay;
|
|
38
|
+
startTimestamp = `${startDay}T00:00:00Z`;
|
|
39
|
+
endTimestamp = `${endDay}T23:59:59Z`;
|
|
40
|
+
} else {
|
|
41
|
+
const defaults = getDefaultDateRange();
|
|
42
|
+
startDay = defaults.startDay;
|
|
43
|
+
endDay = defaults.endDay;
|
|
44
|
+
startTimestamp = `${startDay}T00:00:00Z`;
|
|
45
|
+
endTimestamp = `${endDay}T23:59:59Z`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Convert dataSources array to comma-separated string
|
|
49
|
+
const dataSourcesParam = args.dataSources?.join(',');
|
|
50
|
+
|
|
51
|
+
// Determine resolution (default: "both")
|
|
52
|
+
const resolution = args.resolution || "both";
|
|
53
|
+
|
|
54
|
+
// Fetch data based on resolution
|
|
55
|
+
const fetchPromises: Promise<Response>[] = [];
|
|
56
|
+
|
|
57
|
+
if (resolution === "daily" || resolution === "both") {
|
|
58
|
+
const dailyBody = buildUrlEncodedBody({
|
|
59
|
+
authenticationToken: authToken,
|
|
60
|
+
startDay,
|
|
61
|
+
endDay,
|
|
62
|
+
dataSources: dataSourcesParam,
|
|
63
|
+
displayTypeName: "true",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
fetchPromises.push(
|
|
67
|
+
fetch(`${THRYVE_API_BASE_URL}/v5/dailyDynamicValues`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
body: dailyBody,
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (resolution === "epoch" || resolution === "both") {
|
|
76
|
+
const epochBody = buildUrlEncodedBody({
|
|
77
|
+
authenticationToken: authToken,
|
|
78
|
+
startTimestamp,
|
|
79
|
+
endTimestamp,
|
|
80
|
+
dataSources: dataSourcesParam,
|
|
81
|
+
displayTypeName: "true",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
fetchPromises.push(
|
|
85
|
+
fetch(`${THRYVE_API_BASE_URL}/v5/dynamicEpochValues`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers,
|
|
88
|
+
body: epochBody,
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const responses = await Promise.all(fetchPromises);
|
|
94
|
+
|
|
95
|
+
// Check for errors
|
|
96
|
+
for (const response of responses) {
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`Thryve API error: ${response.status} ${response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse responses
|
|
103
|
+
const jsonData = await Promise.all(responses.map(r => r.json()));
|
|
104
|
+
|
|
105
|
+
// Flatten and enrich data
|
|
106
|
+
let allData: HealthDataRecord[] = [];
|
|
107
|
+
for (const data of jsonData) {
|
|
108
|
+
const flattened = flattenThryveResponse(data);
|
|
109
|
+
enrichWithDataTypes(flattened);
|
|
110
|
+
allData = allData.concat(flattened);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Apply filters
|
|
114
|
+
let filteredData = allData;
|
|
115
|
+
|
|
116
|
+
// Filter by categories (convert to data type IDs)
|
|
117
|
+
if (args.categories && args.categories.length > 0) {
|
|
118
|
+
const categoryDataTypeIds = getDataTypeIdsForCategories(args.categories as HealthCategory[]);
|
|
119
|
+
const dataTypeIdSet = new Set(categoryDataTypeIds);
|
|
120
|
+
|
|
121
|
+
filteredData = filteredData.filter((record) => {
|
|
122
|
+
const typeId = record.dataType || record.dailyDynamicValueType || record.dynamicEpochValueType;
|
|
123
|
+
return typeId && dataTypeIdSet.has(typeId);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Filter by specific data type IDs
|
|
128
|
+
if (args.dataTypes && args.dataTypes.length > 0) {
|
|
129
|
+
const dataTypeIdSet = new Set(args.dataTypes);
|
|
130
|
+
filteredData = filteredData.filter((record) => {
|
|
131
|
+
const typeId = record.dataType || record.dailyDynamicValueType || record.dynamicEpochValueType;
|
|
132
|
+
return typeId !== undefined && dataTypeIdSet.has(typeId);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Filter by value range
|
|
137
|
+
if (args.valueMin !== undefined || args.valueMax !== undefined) {
|
|
138
|
+
filteredData = filteredData.filter((record) => {
|
|
139
|
+
const value = typeof record.value === "string" ? parseFloat(record.value) : record.value;
|
|
140
|
+
if (isNaN(value)) return false;
|
|
141
|
+
|
|
142
|
+
if (args.valueMin !== undefined && value < args.valueMin) return false;
|
|
143
|
+
if (args.valueMax !== undefined && value > args.valueMax) return false;
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Apply limit if specified (before pagination/grouping)
|
|
150
|
+
if (args.limit) {
|
|
151
|
+
filteredData = filteredData.slice(0, args.limit);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if we should group by category
|
|
155
|
+
if (args.groupByCategory && args.categories && args.categories.length > 0) {
|
|
156
|
+
// Group results by category (like getDailyData/getEpochData)
|
|
157
|
+
const groupedResults = groupByCategory(filteredData, args.categories as HealthCategory[]);
|
|
158
|
+
|
|
159
|
+
// Build metadata
|
|
160
|
+
const dataTypesFound = new Set<string>();
|
|
161
|
+
filteredData.forEach((record) => {
|
|
162
|
+
const typeName = record.dailyDynamicValueTypeName || record.dynamicEpochValueTypeName;
|
|
163
|
+
if (typeName) dataTypesFound.add(typeName);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const metadata = {
|
|
167
|
+
resolution,
|
|
168
|
+
categories: args.categories,
|
|
169
|
+
timeRange: {
|
|
170
|
+
start: startTimestamp,
|
|
171
|
+
end: endTimestamp,
|
|
172
|
+
},
|
|
173
|
+
totalRecords: filteredData.length,
|
|
174
|
+
dataTypesFound: Array.from(dataTypesFound).sort(),
|
|
175
|
+
dataSources: args.dataSources,
|
|
176
|
+
filters: {
|
|
177
|
+
dataTypes: args.dataTypes,
|
|
178
|
+
valueMin: args.valueMin,
|
|
179
|
+
valueMax: args.valueMax,
|
|
180
|
+
},
|
|
181
|
+
limitApplied: args.limit,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
results: groupedResults,
|
|
186
|
+
metadata,
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
// Return flat paginated results (original behavior)
|
|
190
|
+
const sortedData = sortHealthData(filteredData, args.sortBy, args.sortOrder);
|
|
191
|
+
const paginatedResult = paginateResults(sortedData, args.page, args.pageSize);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...paginatedResult,
|
|
195
|
+
resolution,
|
|
196
|
+
filters: {
|
|
197
|
+
dataTypes: args.dataTypes,
|
|
198
|
+
dataSources: args.dataSources,
|
|
199
|
+
categories: args.categories,
|
|
200
|
+
startDate: startDay,
|
|
201
|
+
endDate: endDay,
|
|
202
|
+
startTimestamp: args.startTimestamp,
|
|
203
|
+
endTimestamp: args.endTimestamp,
|
|
204
|
+
valueMin: args.valueMin,
|
|
205
|
+
valueMax: args.valueMax,
|
|
206
|
+
limitApplied: args.limit,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Group records by requested categories
|
|
214
|
+
* (Copied from getDailyData/getEpochData for compatibility)
|
|
215
|
+
*/
|
|
216
|
+
function groupByCategory(
|
|
217
|
+
records: HealthDataRecord[],
|
|
218
|
+
requestedCategories: HealthCategory[]
|
|
219
|
+
): Record<string, HealthDataRecord[]> {
|
|
220
|
+
const grouped: Record<string, HealthDataRecord[]> = {};
|
|
221
|
+
|
|
222
|
+
// Initialize structure for requested categories
|
|
223
|
+
for (const category of requestedCategories) {
|
|
224
|
+
grouped[category] = [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get category data type IDs for matching
|
|
228
|
+
const categoryDataTypes = new Map<number, HealthCategory[]>();
|
|
229
|
+
for (const category of requestedCategories) {
|
|
230
|
+
const mapping = CATEGORY_MAPPINGS[category];
|
|
231
|
+
if (mapping) {
|
|
232
|
+
for (const dataTypeId of mapping.dataTypeIds) {
|
|
233
|
+
if (!categoryDataTypes.has(dataTypeId)) {
|
|
234
|
+
categoryDataTypes.set(dataTypeId, []);
|
|
235
|
+
}
|
|
236
|
+
categoryDataTypes.get(dataTypeId)!.push(category);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Group records
|
|
242
|
+
for (const record of records) {
|
|
243
|
+
const typeId = record.dataType || record.dailyDynamicValueType || record.dynamicEpochValueType;
|
|
244
|
+
if (typeId && categoryDataTypes.has(typeId)) {
|
|
245
|
+
const categories = categoryDataTypes.get(typeId)!;
|
|
246
|
+
for (const category of categories) {
|
|
247
|
+
grouped[category].push(record);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return grouped;
|
|
253
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Request } from "express";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { SearchKeywordSchema } from "../schemas.js";
|
|
4
|
+
import type { HealthDataRecord } from "../types.js";
|
|
5
|
+
import {
|
|
6
|
+
extractThryveHeaders,
|
|
7
|
+
buildUrlEncodedBody,
|
|
8
|
+
getDefaultDateRange,
|
|
9
|
+
THRYVE_API_BASE_URL,
|
|
10
|
+
sortHealthData,
|
|
11
|
+
paginateResults,
|
|
12
|
+
} from "../helpers.js";
|
|
13
|
+
import { ALL_DATA_TYPES } from "../data-types-generated.js";
|
|
14
|
+
import { flattenThryveResponse, enrichWithDataTypes } from "../data-enrichment.js";
|
|
15
|
+
|
|
16
|
+
export async function searchKeyword(req: Request, args: z.infer<typeof SearchKeywordSchema>) {
|
|
17
|
+
const { headers, authToken } = extractThryveHeaders(req);
|
|
18
|
+
|
|
19
|
+
// Step 1: Search for matching property names in ALL_DATA_TYPES
|
|
20
|
+
const lowerQuery = args.query.toLowerCase();
|
|
21
|
+
const matchingDataTypes = ALL_DATA_TYPES.filter((dataType) =>
|
|
22
|
+
dataType.name.toLowerCase().includes(lowerQuery)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (matchingDataTypes.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
data: [],
|
|
28
|
+
totalRecords: 0,
|
|
29
|
+
page: args.page,
|
|
30
|
+
pageSize: args.pageSize,
|
|
31
|
+
totalPages: 0,
|
|
32
|
+
query: args.query,
|
|
33
|
+
matchingPropertyNames: [],
|
|
34
|
+
message: `No property names found matching "${args.query}"`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Step 2: Get the data type IDs for matching properties
|
|
39
|
+
const matchingDataTypeIds = new Set(matchingDataTypes.map((dt) => dt.id));
|
|
40
|
+
const matchingPropertyNames = matchingDataTypes.map((dt) => dt.name);
|
|
41
|
+
|
|
42
|
+
// Determine date range
|
|
43
|
+
let startDay: string;
|
|
44
|
+
let endDay: string;
|
|
45
|
+
let startTimestamp: string;
|
|
46
|
+
let endTimestamp: string;
|
|
47
|
+
|
|
48
|
+
if (args.startDate || args.endDate) {
|
|
49
|
+
const defaults = getDefaultDateRange();
|
|
50
|
+
startDay = args.startDate || defaults.startDay;
|
|
51
|
+
endDay = args.endDate || defaults.endDay;
|
|
52
|
+
startTimestamp = `${startDay}T00:00:00Z`;
|
|
53
|
+
endTimestamp = `${endDay}T23:59:59Z`;
|
|
54
|
+
} else {
|
|
55
|
+
const defaults = getDefaultDateRange();
|
|
56
|
+
startDay = defaults.startDay;
|
|
57
|
+
endDay = defaults.endDay;
|
|
58
|
+
startTimestamp = `${startDay}T00:00:00Z`;
|
|
59
|
+
endTimestamp = `${endDay}T23:59:59Z`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Step 3: Determine which endpoint(s) to fetch from based on temporal resolution
|
|
63
|
+
const needsDaily = matchingDataTypes.some((dt) =>
|
|
64
|
+
dt.temporalResolution === "Daily" || dt.temporalResolution === "Both"
|
|
65
|
+
);
|
|
66
|
+
const needsEpoch = matchingDataTypes.some((dt) =>
|
|
67
|
+
dt.temporalResolution === "Epoch" || dt.temporalResolution === "Both"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const fetchPromises = [];
|
|
71
|
+
|
|
72
|
+
// Fetch daily data if needed
|
|
73
|
+
if (needsDaily) {
|
|
74
|
+
const dailyBody = buildUrlEncodedBody({
|
|
75
|
+
authenticationToken: authToken,
|
|
76
|
+
startDay,
|
|
77
|
+
endDay,
|
|
78
|
+
dataSources: args.dataSource,
|
|
79
|
+
displayTypeName: "true",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
fetchPromises.push(
|
|
83
|
+
fetch(`${THRYVE_API_BASE_URL}/v5/dailyDynamicValues`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers,
|
|
86
|
+
body: dailyBody,
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fetch epoch data if needed
|
|
92
|
+
if (needsEpoch) {
|
|
93
|
+
const epochBody = buildUrlEncodedBody({
|
|
94
|
+
authenticationToken: authToken,
|
|
95
|
+
startTimestamp,
|
|
96
|
+
endTimestamp,
|
|
97
|
+
dataSources: args.dataSource,
|
|
98
|
+
displayTypeName: "true",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
fetchPromises.push(
|
|
102
|
+
fetch(`${THRYVE_API_BASE_URL}/v5/dynamicEpochValues`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers,
|
|
105
|
+
body: epochBody,
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const responses = await Promise.all(fetchPromises);
|
|
111
|
+
|
|
112
|
+
// Check for errors
|
|
113
|
+
for (const response of responses) {
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`Thryve API error: ${response.status} ${response.statusText}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parse responses
|
|
120
|
+
const jsonData = await Promise.all(responses.map((r) => r.json()));
|
|
121
|
+
|
|
122
|
+
// Flatten and enrich data
|
|
123
|
+
let allData: HealthDataRecord[] = [];
|
|
124
|
+
for (const data of jsonData) {
|
|
125
|
+
const flattened = flattenThryveResponse(data);
|
|
126
|
+
enrichWithDataTypes(flattened);
|
|
127
|
+
allData = allData.concat(flattened);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Step 4: Filter by matching data type IDs
|
|
131
|
+
let filteredData = allData.filter((record) => {
|
|
132
|
+
const typeId = record.dataType || record.dailyDynamicValueType || record.dynamicEpochValueType;
|
|
133
|
+
return typeId && matchingDataTypeIds.has(typeId);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Optional: Filter by data type category if specified
|
|
137
|
+
if (args.dataType) {
|
|
138
|
+
const lowerDataType = args.dataType.toLowerCase();
|
|
139
|
+
filteredData = filteredData.filter((record) => {
|
|
140
|
+
const typeName = record.dailyDynamicValueTypeName || record.dynamicEpochValueTypeName || "";
|
|
141
|
+
return typeName.toLowerCase().includes(lowerDataType);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sort the results
|
|
146
|
+
const sortedData = sortHealthData(filteredData, args.sortBy, args.sortOrder);
|
|
147
|
+
|
|
148
|
+
// Paginate the results
|
|
149
|
+
const paginatedResult = paginateResults(sortedData, args.page, args.pageSize);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...paginatedResult,
|
|
153
|
+
query: args.query,
|
|
154
|
+
matchingPropertyNames,
|
|
155
|
+
filters: {
|
|
156
|
+
startDate: startDay,
|
|
157
|
+
endDate: endDay,
|
|
158
|
+
dataType: args.dataType,
|
|
159
|
+
dataSource: args.dataSource,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Type for health data records (unified format for daily and epoch data)
|
|
2
|
+
export interface HealthDataRecord {
|
|
3
|
+
day?: string;
|
|
4
|
+
timestamp?: string;
|
|
5
|
+
value: string | number;
|
|
6
|
+
dataType?: number;
|
|
7
|
+
dataTypeName?: string;
|
|
8
|
+
dailyDynamicValueType?: number;
|
|
9
|
+
dynamicEpochValueType?: number;
|
|
10
|
+
dailyDynamicValueTypeName?: string;
|
|
11
|
+
dynamicEpochValueTypeName?: string;
|
|
12
|
+
dataSource?: number;
|
|
13
|
+
dataSourceName?: string;
|
|
14
|
+
unit?: string;
|
|
15
|
+
category?: string;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Pagination result structure
|
|
20
|
+
export interface PaginationMetadata {
|
|
21
|
+
currentPage: number;
|
|
22
|
+
pageSize: number;
|
|
23
|
+
totalResults: number;
|
|
24
|
+
totalPages: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PaginatedResult<T> {
|
|
28
|
+
results: T[];
|
|
29
|
+
pagination: PaginationMetadata;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parsed record ID structure
|
|
33
|
+
export interface ParsedRecordId {
|
|
34
|
+
type: 'daily' | 'epoch';
|
|
35
|
+
dataType: string;
|
|
36
|
+
timeKey: string;
|
|
37
|
+
dataSource: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Thryve API headers structure
|
|
41
|
+
export interface ThryveHeaders {
|
|
42
|
+
headers: Record<string, string>;
|
|
43
|
+
authToken: string;
|
|
44
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|