@adventurelabs/scout-core 1.4.56 → 1.4.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants/db.d.ts +2 -0
- package/dist/constants/db.js +2 -1
- package/dist/helpers/deviceTransfer.d.ts +2 -0
- package/dist/helpers/deviceTransfer.js +14 -0
- package/dist/helpers/storage.d.ts +5 -7
- package/dist/helpers/storage.js +18 -54
- package/dist/providers/ScoutRefreshProvider.d.ts +9 -0
- package/dist/store/api.d.ts +5 -0
- package/dist/store/api.js +40 -40
- package/dist/types/supabase.d.ts +9 -0
- package/package.json +3 -2
package/dist/constants/db.d.ts
CHANGED
package/dist/constants/db.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export const BUCKET_NAME_SCOUT = "scout";
|
|
2
|
-
|
|
2
|
+
/** Event media when `file_path` uses the `events/...` prefix */
|
|
3
|
+
export const BUCKET_NAME_EVENTS = "events";
|
|
3
4
|
export const SIGNED_URL_EXPIRATION_SECONDS = 12 * 60 * 60;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { newServerClient } from "../supabase/server";
|
|
3
|
+
import { IWebResponse } from "../types/requests";
|
|
4
|
+
export async function transferDeviceToHerd(deviceId, targetHerdId) {
|
|
5
|
+
const client = await newServerClient();
|
|
6
|
+
const { error } = await client.rpc("transfer_device_to_herd", {
|
|
7
|
+
p_device_id: deviceId,
|
|
8
|
+
p_target_herd_id: targetHerdId,
|
|
9
|
+
});
|
|
10
|
+
if (error) {
|
|
11
|
+
return IWebResponse.error(error.message).to_compatible();
|
|
12
|
+
}
|
|
13
|
+
return IWebResponse.success(true).to_compatible();
|
|
14
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { Database } from "../types/supabase";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* @returns Promise<string | null> - The signed URL or null if error
|
|
9
|
-
*/
|
|
3
|
+
/** DB `file_path` is always `bucket/object/...` (e.g. `events/254/...`, `artifacts/254/...`). */
|
|
4
|
+
export declare function parseStorageFilePath(filePath: string): {
|
|
5
|
+
bucket: string;
|
|
6
|
+
objectPath: string;
|
|
7
|
+
} | null;
|
|
10
8
|
export declare function generateSignedUrl(filePath: string, expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<string | null>;
|
|
11
9
|
export declare function generateSignedUrlsBatch(filePaths: string[], expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<(string | null)[]>;
|
package/dist/helpers/storage.js
CHANGED
|
@@ -1,64 +1,33 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { newServerClient } from "../supabase/server";
|
|
3
|
-
import { SIGNED_URL_EXPIRATION_SECONDS
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
function getBucketFromFilePath(filePath) {
|
|
10
|
-
// delete any start or end slashes/whitespace
|
|
11
|
-
filePath = filePath.replace(/^\/+|\/+$/g, "");
|
|
12
|
-
const parts = filePath.split("/");
|
|
13
|
-
if (parts.length < 2) {
|
|
3
|
+
import { SIGNED_URL_EXPIRATION_SECONDS } from "../constants/db";
|
|
4
|
+
/** DB `file_path` is always `bucket/object/...` (e.g. `events/254/...`, `artifacts/254/...`). */
|
|
5
|
+
export function parseStorageFilePath(filePath) {
|
|
6
|
+
const cleaned = filePath.trim().replace(/^\/+|\/+$/g, "");
|
|
7
|
+
if (!cleaned) {
|
|
14
8
|
return null;
|
|
15
9
|
}
|
|
16
|
-
const bucket_name = parts[0];
|
|
17
|
-
return bucket_name;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Extracts the short path from a file path
|
|
21
|
-
* @param filePath
|
|
22
|
-
* @returns string | null - null if invalid file path
|
|
23
|
-
*/
|
|
24
|
-
// for example if the input is /artifacts/10/52/test.mp4 - the output shld be 10/52/test.mp4
|
|
25
|
-
function getFormattedPath(filePath) {
|
|
26
|
-
const cleaned = cleanPath(filePath);
|
|
27
10
|
const parts = cleaned.split("/");
|
|
28
11
|
if (parts.length < 2) {
|
|
29
12
|
return null;
|
|
30
13
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// delete leading/trailing slash and whitespace
|
|
37
|
-
return filePath.trim().replace(/^\/+|\/+$/g, "");
|
|
14
|
+
const bucket = parts[0];
|
|
15
|
+
if (/^[0-9]+$/.test(bucket)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return { bucket, objectPath: parts.slice(1).join("/") };
|
|
38
19
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Generates a signed URL for a file in Supabase storage
|
|
41
|
-
* @param filePath - The path to the file in storage (e.g., "events/123/image.jpg")
|
|
42
|
-
* @param expiresIn - Number of seconds until the URL expires (default: 12 hours)
|
|
43
|
-
* @param supabaseClient - Optional Supabase client (will create new one if not provided)
|
|
44
|
-
* @returns Promise<string | null> - The signed URL or null if error
|
|
45
|
-
*/
|
|
46
20
|
export async function generateSignedUrl(filePath, expiresIn = SIGNED_URL_EXPIRATION_SECONDS, supabaseClient) {
|
|
47
21
|
try {
|
|
48
22
|
const supabase = supabaseClient || (await newServerClient());
|
|
49
|
-
const
|
|
50
|
-
if (!
|
|
23
|
+
const parsed = parseStorageFilePath(filePath);
|
|
24
|
+
if (!parsed) {
|
|
51
25
|
console.error("Invalid file path:", filePath);
|
|
52
26
|
return null;
|
|
53
27
|
}
|
|
54
|
-
const formattedPath = getFormattedPath(filePath);
|
|
55
|
-
if (!formattedPath) {
|
|
56
|
-
console.error("Invalid formatted path:", formattedPath);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
28
|
const { data, error } = await supabase.storage
|
|
60
|
-
.from(
|
|
61
|
-
.createSignedUrl(
|
|
29
|
+
.from(parsed.bucket)
|
|
30
|
+
.createSignedUrl(parsed.objectPath, expiresIn);
|
|
62
31
|
if (error) {
|
|
63
32
|
console.error("Error generating signed URL:", error.message);
|
|
64
33
|
return null;
|
|
@@ -75,19 +44,14 @@ export async function generateSignedUrlsBatch(filePaths, expiresIn = SIGNED_URL_
|
|
|
75
44
|
const supabase = supabaseClient || (await newServerClient());
|
|
76
45
|
const signedUrlPromises = filePaths.map(async (filePath) => {
|
|
77
46
|
try {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
47
|
+
const parsed = parseStorageFilePath(filePath);
|
|
48
|
+
if (!parsed) {
|
|
80
49
|
console.error("Invalid file path:", filePath);
|
|
81
50
|
return null;
|
|
82
51
|
}
|
|
83
|
-
const formattedPath = getFormattedPath(filePath);
|
|
84
|
-
if (!formattedPath) {
|
|
85
|
-
console.error("Invalid formatted path:", formattedPath);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
52
|
const { data, error } = await supabase.storage
|
|
89
|
-
.from(
|
|
90
|
-
.createSignedUrl(
|
|
53
|
+
.from(parsed.bucket)
|
|
54
|
+
.createSignedUrl(parsed.objectPath, expiresIn);
|
|
91
55
|
if (error) {
|
|
92
56
|
console.warn(`Error generating signed URL for ${filePath}:`, error.message);
|
|
93
57
|
return null;
|
|
@@ -2488,6 +2488,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
|
|
|
2488
2488
|
limit_caller?: number;
|
|
2489
2489
|
min_flight_distance_meters?: number;
|
|
2490
2490
|
min_flight_time_minutes?: number;
|
|
2491
|
+
product_numbers_caller?: string[];
|
|
2491
2492
|
range_end?: string;
|
|
2492
2493
|
range_start?: string;
|
|
2493
2494
|
};
|
|
@@ -2507,6 +2508,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
|
|
|
2507
2508
|
limit_caller?: number;
|
|
2508
2509
|
min_flight_distance_meters?: number;
|
|
2509
2510
|
min_flight_time_minutes?: number;
|
|
2511
|
+
product_numbers_caller?: string[];
|
|
2510
2512
|
range_end?: string;
|
|
2511
2513
|
range_start?: string;
|
|
2512
2514
|
};
|
|
@@ -2770,6 +2772,13 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
|
|
|
2770
2772
|
status: string;
|
|
2771
2773
|
}[];
|
|
2772
2774
|
};
|
|
2775
|
+
transfer_device_to_herd: {
|
|
2776
|
+
Args: {
|
|
2777
|
+
p_device_id: number;
|
|
2778
|
+
p_target_herd_id: number;
|
|
2779
|
+
};
|
|
2780
|
+
Returns: undefined;
|
|
2781
|
+
};
|
|
2773
2782
|
};
|
|
2774
2783
|
Enums: {
|
|
2775
2784
|
analysis_work_status: "waiting" | "cancelled" | "processing" | "failed" | "success";
|
package/dist/store/api.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface InfiniteQueryArgs {
|
|
|
15
15
|
minFlightTimeMinutes?: number | null;
|
|
16
16
|
/** Sessions RPC only: minimum distance_total in meters. */
|
|
17
17
|
minFlightDistanceMeters?: number | null;
|
|
18
|
+
/**
|
|
19
|
+
* Sessions infinite RPCs only: filter to devices with a matching non-deleted part.
|
|
20
|
+
* Omitted/null = no filter; empty array = no sessions.
|
|
21
|
+
*/
|
|
22
|
+
productNumbers?: string[] | null;
|
|
18
23
|
}
|
|
19
24
|
export interface SessionsInfiniteResponse {
|
|
20
25
|
sessions: ISessionWithCoordinates[];
|
package/dist/store/api.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
|
|
2
2
|
import { generateSignedUrlsBatch } from "../helpers/storage";
|
|
3
|
+
async function signedUrlMapForFeedRows(rows) {
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const paths = [];
|
|
6
|
+
for (const row of rows) {
|
|
7
|
+
const ep = row.event_data?.file_path;
|
|
8
|
+
if (ep && !seen.has(ep)) {
|
|
9
|
+
seen.add(ep);
|
|
10
|
+
paths.push(ep);
|
|
11
|
+
}
|
|
12
|
+
const ap = row.artifact_data?.file_path;
|
|
13
|
+
if (ap && !seen.has(ap)) {
|
|
14
|
+
seen.add(ap);
|
|
15
|
+
paths.push(ap);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const map = new Map();
|
|
19
|
+
if (paths.length === 0) {
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
const r = await generateSignedUrlsBatch(paths);
|
|
23
|
+
paths.forEach((p, i) => {
|
|
24
|
+
if (r[i])
|
|
25
|
+
map.set(p, r[i]);
|
|
26
|
+
});
|
|
27
|
+
return map;
|
|
28
|
+
}
|
|
3
29
|
// Custom serialize function to exclude supabase client
|
|
4
30
|
const serializeQueryArgs = ({ queryArgs, endpointDefinition, endpointName, }) => {
|
|
5
31
|
const { supabase, ...serializableArgs } = queryArgs;
|
|
@@ -16,7 +42,7 @@ export const scoutApi = createApi({
|
|
|
16
42
|
// =====================================================
|
|
17
43
|
getSessionsInfiniteByHerd: builder.query({
|
|
18
44
|
serializeQueryArgs,
|
|
19
|
-
async queryFn({ herdId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, supabase, }) {
|
|
45
|
+
async queryFn({ herdId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, productNumbers, supabase, }) {
|
|
20
46
|
try {
|
|
21
47
|
if (!herdId) {
|
|
22
48
|
return {
|
|
@@ -32,6 +58,7 @@ export const scoutApi = createApi({
|
|
|
32
58
|
range_end: rangeEnd ?? null,
|
|
33
59
|
min_flight_time_minutes: minFlightTimeMinutes ?? null,
|
|
34
60
|
min_flight_distance_meters: minFlightDistanceMeters ?? null,
|
|
61
|
+
product_numbers_caller: productNumbers ?? undefined,
|
|
35
62
|
});
|
|
36
63
|
if (error) {
|
|
37
64
|
return {
|
|
@@ -72,7 +99,7 @@ export const scoutApi = createApi({
|
|
|
72
99
|
}),
|
|
73
100
|
getSessionsInfiniteByDevice: builder.query({
|
|
74
101
|
serializeQueryArgs,
|
|
75
|
-
async queryFn({ deviceId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, supabase, }) {
|
|
102
|
+
async queryFn({ deviceId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, productNumbers, supabase, }) {
|
|
76
103
|
try {
|
|
77
104
|
if (!deviceId) {
|
|
78
105
|
return {
|
|
@@ -88,6 +115,7 @@ export const scoutApi = createApi({
|
|
|
88
115
|
range_end: rangeEnd ?? null,
|
|
89
116
|
min_flight_time_minutes: minFlightTimeMinutes ?? null,
|
|
90
117
|
min_flight_distance_meters: minFlightDistanceMeters ?? null,
|
|
118
|
+
product_numbers_caller: productNumbers ?? undefined,
|
|
91
119
|
});
|
|
92
120
|
if (error) {
|
|
93
121
|
return {
|
|
@@ -476,26 +504,12 @@ export const scoutApi = createApi({
|
|
|
476
504
|
// Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
|
|
477
505
|
const hasMore = rows.length >= limit;
|
|
478
506
|
const resultRows = hasMore ? rows.slice(0, limit) : rows;
|
|
479
|
-
const uniqueFilePaths = Array.from(new Set(resultRows.flatMap((row) => {
|
|
480
|
-
const paths = [];
|
|
481
|
-
if (row.event_data?.file_path)
|
|
482
|
-
paths.push(row.event_data.file_path);
|
|
483
|
-
if (row.artifact_data?.file_path)
|
|
484
|
-
paths.push(row.artifact_data.file_path);
|
|
485
|
-
return paths;
|
|
486
|
-
})));
|
|
487
507
|
let urlMap = new Map();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
urlMap.set(uniqueFilePaths[index], url);
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
catch (urlError) {
|
|
497
|
-
console.warn("Failed to generate signed URLs for feed:", urlError);
|
|
498
|
-
}
|
|
508
|
+
try {
|
|
509
|
+
urlMap = await signedUrlMapForFeedRows(resultRows);
|
|
510
|
+
}
|
|
511
|
+
catch (urlError) {
|
|
512
|
+
console.warn("Failed to generate signed URLs for feed:", urlError);
|
|
499
513
|
}
|
|
500
514
|
const items = resultRows.map((row) => ({
|
|
501
515
|
...row,
|
|
@@ -565,26 +579,12 @@ export const scoutApi = createApi({
|
|
|
565
579
|
// Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
|
|
566
580
|
const hasMore = rows.length >= limit;
|
|
567
581
|
const resultRows = hasMore ? rows.slice(0, limit) : rows;
|
|
568
|
-
const uniqueFilePaths = Array.from(new Set(resultRows.flatMap((row) => {
|
|
569
|
-
const paths = [];
|
|
570
|
-
if (row.event_data?.file_path)
|
|
571
|
-
paths.push(row.event_data.file_path);
|
|
572
|
-
if (row.artifact_data?.file_path)
|
|
573
|
-
paths.push(row.artifact_data.file_path);
|
|
574
|
-
return paths;
|
|
575
|
-
})));
|
|
576
582
|
let urlMap = new Map();
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
urlMap.set(uniqueFilePaths[index], url);
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
catch (urlError) {
|
|
586
|
-
console.warn("Failed to generate signed URLs for feed:", urlError);
|
|
587
|
-
}
|
|
583
|
+
try {
|
|
584
|
+
urlMap = await signedUrlMapForFeedRows(resultRows);
|
|
585
|
+
}
|
|
586
|
+
catch (urlError) {
|
|
587
|
+
console.warn("Failed to generate signed URLs for feed:", urlError);
|
|
588
588
|
}
|
|
589
589
|
const items = resultRows.map((row) => ({
|
|
590
590
|
...row,
|
package/dist/types/supabase.d.ts
CHANGED
|
@@ -2573,6 +2573,7 @@ export type Database = {
|
|
|
2573
2573
|
limit_caller?: number;
|
|
2574
2574
|
min_flight_distance_meters?: number;
|
|
2575
2575
|
min_flight_time_minutes?: number;
|
|
2576
|
+
product_numbers_caller?: string[];
|
|
2576
2577
|
range_end?: string;
|
|
2577
2578
|
range_start?: string;
|
|
2578
2579
|
};
|
|
@@ -2592,6 +2593,7 @@ export type Database = {
|
|
|
2592
2593
|
limit_caller?: number;
|
|
2593
2594
|
min_flight_distance_meters?: number;
|
|
2594
2595
|
min_flight_time_minutes?: number;
|
|
2596
|
+
product_numbers_caller?: string[];
|
|
2595
2597
|
range_end?: string;
|
|
2596
2598
|
range_start?: string;
|
|
2597
2599
|
};
|
|
@@ -2855,6 +2857,13 @@ export type Database = {
|
|
|
2855
2857
|
status: string;
|
|
2856
2858
|
}[];
|
|
2857
2859
|
};
|
|
2860
|
+
transfer_device_to_herd: {
|
|
2861
|
+
Args: {
|
|
2862
|
+
p_device_id: number;
|
|
2863
|
+
p_target_herd_id: number;
|
|
2864
|
+
};
|
|
2865
|
+
Returns: undefined;
|
|
2866
|
+
};
|
|
2858
2867
|
};
|
|
2859
2868
|
Enums: {
|
|
2860
2869
|
analysis_work_status: "waiting" | "cancelled" | "processing" | "failed" | "success";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adventurelabs/scout-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.58",
|
|
4
4
|
"description": "Core utilities and helpers for Adventure Labs Scout applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"prepare": "yarn build",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
|
-
"clean": "rm -rf dist"
|
|
15
|
+
"clean": "rm -rf dist",
|
|
16
|
+
"migrate-storage-paths-device-prefix": "node scripts/migrate-storage-paths-device-prefix.mjs"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"scout",
|